├── .gitignore ├── remarkdown ├── __init__.py ├── scripts.py ├── parser.py └── markdown.parsley ├── setup.py ├── license.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *egg-info 3 | -------------------------------------------------------------------------------- /remarkdown/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | File: setup.py 5 | Author: Steve Genoud 6 | Date: 2013-08-25 7 | ''' 8 | from setuptools import setup 9 | import remarkdown 10 | 11 | setup(name='remarkdown', 12 | version=remarkdown.__version__, 13 | install_requires=[ 14 | 'Parsley>= 1.2', 15 | 'docutils>=0.11' 16 | ], 17 | entry_points={'console_scripts': [ 18 | 'md2html = remarkdown.scripts:md2html', 19 | 'md2xml = remarkdown.scripts:md2xml', 20 | 'md2pseudoxml = remarkdown.scripts:md2pseudoxml', 21 | 'md2latex = remarkdown.scripts:md2latex', 22 | 'md2xetex = remarkdown.scripts:md2xetex', 23 | ]}, 24 | package_data = { 25 | '': ['*.parsley'] 26 | }, 27 | packages=['remarkdown'] 28 | ) 29 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Steve Genoud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remarkdown 2 | 3 | A markdown parser based on `docutils` 4 | 5 | **Note that this code is still alpha, some markdown features might not work yet** 6 | 7 | ## Why another markdown library? 8 | 9 | remarkdown is not just only another markdown library. It mostly contains a parser 10 | that outputs a [`docutils` document tree][docutils]. The different scripts 11 | bundled then use `docutils` for generation of different types of documents. 12 | 13 | Why is this important? Many python tools (mostly for documentation creation) 14 | rely on `docutils`. But `docutils` only supports a ReStructuredText syntax. For 15 | instance [this issue][sphinx-issue] and [this StackOverflow 16 | question][so-question] show that there is an interest in allowing `docutils` to 17 | use markdown as an alternative syntax. 18 | 19 | [docutils]: http://docutils.sourceforge.net/docs/ref/doctree.html 20 | [sphinx-issue]: https://bitbucket.org/birkenfeld/sphinx/issue/825/markdown-capable-sphinx 21 | [so-question]: http://stackoverflow.com/questions/2471804/using-sphinx-with-markdown-instead-of-rst 22 | 23 | ## Acknowledgement 24 | 25 | The remarkdown PEG is heavily inspired by [peg-markdown by John 26 | MacFarlane][peg-md]. 27 | 28 | 29 | [peg-md]: https://github.com/jgm/peg-markdown 30 | 31 | 32 | -------------------------------------------------------------------------------- /remarkdown/scripts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | File: scripts.py 5 | Author: Steve Genoud 6 | Date: 2013-08-25 7 | Description: Scripts loaded by setuptools entry points 8 | ''' 9 | 10 | 11 | try: 12 | import locale 13 | locale.setlocale(locale.LC_ALL, '') 14 | except: 15 | pass 16 | 17 | from docutils.core import publish_cmdline, default_description 18 | from remarkdown.parser import MarkdownParser 19 | 20 | 21 | def md2html(): 22 | description = ('Generate html document from markdown sources. ' 23 | + default_description) 24 | publish_cmdline(writer_name='html', 25 | parser=MarkdownParser(), 26 | description=description) 27 | 28 | def md2xml(): 29 | description = ('Generate XML document from markdown sources. ' 30 | + default_description) 31 | publish_cmdline(writer_name='xml', 32 | parser=MarkdownParser(), 33 | description=description) 34 | def md2pseudoxml(): 35 | description = ('Generate pseudo-XML document from markdown sources. ' 36 | + default_description) 37 | publish_cmdline(writer_name='pseudoxml', 38 | parser=MarkdownParser(), 39 | description=description) 40 | 41 | def md2latex(): 42 | description = ('Generate latex document from markdown sources. ' 43 | + default_description) 44 | publish_cmdline(writer_name='latex', 45 | parser=MarkdownParser(), 46 | description=description) 47 | 48 | def md2xetex(): 49 | description = ('Generate xetex document from markdown sources. ' 50 | + default_description) 51 | publish_cmdline(writer_name='latex', 52 | parser=MarkdownParser(), 53 | description=description) 54 | 55 | -------------------------------------------------------------------------------- /remarkdown/parser.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import itertools 3 | import os.path 4 | 5 | from docutils import parsers, nodes 6 | import parsley 7 | 8 | __all__ = ['MarkdownParser'] 9 | 10 | def flatten(iterator): 11 | return itertools.chain.from_iterable(iterator) 12 | 13 | 14 | class _SectionHandler(object): 15 | def __init__(self, document): 16 | self._level_to_elem = {0: document} 17 | 18 | def _parent_elem(self, child_level): 19 | parent_level = max(level for level in self._level_to_elem 20 | if child_level > level) 21 | return self._level_to_elem[parent_level] 22 | 23 | def _prune_levels(self, limit_level): 24 | self._level_to_elem = dict((level, elem) 25 | for level, elem in self._level_to_elem.items() 26 | if level <= limit_level) 27 | 28 | def add_new_section(self, section, level): 29 | 30 | parent = self._parent_elem(level) 31 | parent.append(section) 32 | self._level_to_elem[level] = section 33 | self._prune_levels(level) 34 | 35 | 36 | class MarkdownParser(object, parsers.Parser): 37 | supported = ('md', 'markdown') 38 | 39 | def parse(self, inputstring, document): 40 | self.setup_parse(inputstring, document) 41 | 42 | self.document = document 43 | self.current_node = document 44 | self.section_handler = _SectionHandler(document) 45 | 46 | base = os.path.dirname(os.path.abspath(__file__)) 47 | filename = os.path.join(base, "markdown.parsley") 48 | 49 | with open(filename) as pry_file: 50 | self.grammar_raw = pry_file.read() 51 | self.grammar = parsley.makeGrammar( 52 | self.grammar_raw, 53 | dict(builder=self), 54 | name='Markdown' 55 | ) 56 | 57 | self.grammar(inputstring + '\n').document() 58 | self.finish_parse() 59 | 60 | @contextmanager 61 | def _temp_current_node(self, current_node): 62 | saved_node = self.current_node 63 | self.current_node = current_node 64 | yield 65 | self.current_node = saved_node 66 | 67 | # Blocks 68 | def section(self, text, level): 69 | new_section = nodes.section() 70 | new_section['level'] = level 71 | 72 | title_node = nodes.title() 73 | append_inlines(title_node, text) 74 | new_section.append(title_node) 75 | 76 | self.section_handler.add_new_section(new_section, level) 77 | self.current_node = new_section 78 | 79 | def verbatim(self, text): 80 | verbatim_node = nodes.literal_block() 81 | text = ''.join(flatten(text)) 82 | if text.endswith('\n'): 83 | text = text[:-1] 84 | verbatim_node.append(nodes.Text(text)) 85 | self.current_node.append(verbatim_node) 86 | 87 | def paragraph(self, text): 88 | p = nodes.paragraph() 89 | append_inlines(p, text) 90 | self.current_node.append(p) 91 | 92 | def quote(self, text): 93 | q = nodes.block_quote() 94 | 95 | with self._temp_current_node(q): 96 | self.grammar(text).document() 97 | 98 | self.current_node.append(q) 99 | 100 | 101 | def _build_list(self, items, node): 102 | for item in items: 103 | list_item = nodes.list_item() 104 | with self._temp_current_node(list_item): 105 | self.grammar(item + "\n\n").document() 106 | 107 | node.append(list_item) 108 | return node 109 | 110 | def bullet_list(self, items): 111 | bullet_list = nodes.bullet_list() 112 | self._build_list(items, bullet_list) 113 | self.current_node.append(bullet_list) 114 | 115 | def ordered_list(self, items): 116 | ordered_list = nodes.enumerated_list() 117 | self._build_list(items, ordered_list) 118 | self.current_node.append(ordered_list) 119 | 120 | 121 | def horizontal_rule(self): 122 | self.current_node.append(nodes.transition()) 123 | 124 | 125 | def target(self, label, uri, title): 126 | target_node = nodes.target() 127 | 128 | target_node['names'].append(make_refname(label)) 129 | 130 | target_node['refuri'] = uri 131 | 132 | if title: 133 | target_node['title'] = title 134 | 135 | self.current_node.append(target_node) 136 | 137 | 138 | # Inlines 139 | def emph(self, inlines): 140 | emph_node = nodes.emphasis() 141 | append_inlines(emph_node, inlines) 142 | return emph_node 143 | 144 | def strong(self, inlines): 145 | strong_node = nodes.strong() 146 | append_inlines(strong_node, inlines) 147 | return strong_node 148 | 149 | def literal(self, inlines): 150 | literal_node = nodes.literal() 151 | append_inlines(literal_node, inlines) 152 | return literal_node 153 | 154 | def reference(self, content, label=None, uri=None, title=None): 155 | 156 | ref_node = nodes.reference() 157 | label = make_refname(content if label is None else label) 158 | 159 | ref_node['name'] = label 160 | if uri is not None: 161 | ref_node['refuri'] = uri 162 | else: 163 | ref_node['refname'] = label 164 | self.document.note_refname(ref_node) 165 | 166 | if title: 167 | ref_node['title'] = title 168 | 169 | append_inlines(ref_node, content) 170 | return ref_node 171 | 172 | 173 | def image(self, content, label=None, uri=None, title=None): 174 | 175 | label = make_refname(content if label is None else label) 176 | if uri is not None: 177 | img_node = nodes.image() 178 | img_node['uri'] = uri 179 | else: 180 | img_node = nodes.substitution_reference() 181 | img_node['refname'] = label 182 | self.document.note_refname(img_node) 183 | 184 | if title: 185 | img_node['title'] = title 186 | 187 | img_node['alt'] = text_only(content) 188 | return img_node 189 | 190 | 191 | def _is_string(val): 192 | return isinstance(val, basestring) 193 | 194 | def make_refname(label): 195 | return text_only(label).lower() 196 | 197 | def text_only(nodes): 198 | return "".join(s if _is_string(s) else text_only(s.children) 199 | for s in nodes) 200 | 201 | def append_inlines(parent_node, inlines): 202 | for is_text, elem_group in itertools.groupby(inlines, _is_string): 203 | if is_text: 204 | parent_node.append(nodes.Text("".join(elem_group))) 205 | else: 206 | map(parent_node.append, elem_group) 207 | 208 | -------------------------------------------------------------------------------- /remarkdown/markdown.parsley: -------------------------------------------------------------------------------- 1 | # Basic Elements 2 | 3 | ## Spaces 4 | 5 | spacechar = ' ' | '\t' 6 | sp = spacechar* 7 | newline = '\n' | '\r' '\n'? 8 | blank_line = sp newline 9 | spnl = sp (newline sp)? 10 | 11 | ## Characters 12 | 13 | nonspacechar = ~spacechar ~newline anything 14 | special_char = '~' | '*' | '_' | '`' | '&' | '[' | ']' | '(' | ')' | '<' | '!' | '#' | '\\' | '\'' | '"' 15 | 16 | normal_char = ~(special_char | spacechar | newline) anything 17 | escapable_char = :x ?(x in "-\\`|*_{}[\]()#+.!><") -> x 18 | ascii_char = :x ?(x in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") -> x 19 | 20 | ## HTML entities 21 | 22 | hex_entity = <'&' '#' ('X' | 'x') hexdigit+ ';'> 23 | dec_entity = <'&' '#' digit+ ';'> 24 | char_entity = <'&' letterOrDigit+ ';'> 25 | 26 | ## Indentation 27 | 28 | indent = ' '{4} | '\t' 29 | nonindent_space = ' '{0,3} 30 | 31 | ## End of Line 32 | 33 | endline = line_break | terminal_endline | normal_endline 34 | normal_endline = sp newline 35 | ~blank_line 36 | ~'>' 37 | ~(line ('='+ | '-'+) newline) 38 | ~atx_start 39 | -> '\n' 40 | terminal_endline = sp newline end -> '\n' 41 | line_break = " " normal_endline -> dict(id_='line_break') 42 | 43 | ## Line definitions 44 | 45 | line = <(~'\r' ~'\n' anything)* newline> | ( end) 46 | indented_line = indent line 47 | optionally_indented_line = indent? line 48 | non_blank_indented_line = ~blank_line indented_line 49 | 50 | 51 | # Inline parsing 52 | 53 | inline = text 54 | | endline 55 | | ul_or_star_line 56 | | space 57 | | emph 58 | | strong 59 | | image 60 | | link 61 | | code 62 | | raw_html 63 | | entity 64 | | escaped_char 65 | | symbol 66 | 67 | inlines = inline* 68 | 69 | space = spacechar+ -> ' ' 70 | 71 | text = 72 | text_chunk = | apos_chunk 73 | # should be part of the smart extension 74 | apos_chunk = '\'' ~~letterOrDigit 75 | -> dict(id_='apostrophe') 76 | 77 | escaped_char = '\\' ~newline 78 | 79 | entity = (hex_entity | dec_entity | char_entity):en 80 | -> dict(id_='html', children=en) 81 | 82 | symbol = 83 | 84 | # This keeps the parser from getting bogged down on long strings of '*' or '_', 85 | # or strings of '*' or '_' with space on each side: 86 | ul_or_star_line = (ul_line | star_line) 87 | star_line = <'****' '*'*> | 88 | ul_line = <'____' '_'*> | 89 | 90 | 91 | whitespace = spacechar | newline 92 | emph = (emph_star | emph_ul):child 93 | -> builder.emph(child) 94 | emph_star = '*' ~whitespace (~'*' inline | strong_star)+:txt '*' 95 | -> txt 96 | emph_ul = '_' ~whitespace (~'_' inline | strong_ul)+:txt '_' 97 | -> txt 98 | 99 | strong = (strong_star | strong_ul):child 100 | -> builder.strong(child) 101 | strong_star = '**' ~whitespace (~'**' inline)+:txt '**' 102 | -> txt 103 | strong_ul = '__' ~whitespace (~'__' inline)+:txt '__' 104 | -> txt 105 | 106 | #TODO: make the ~^ part of the notes extentions 107 | label = '[' (~'^') 108 | (~']' inline)*:label_elements 109 | ']' 110 | -> label_elements 111 | 112 | image = '!' (explicit_link | reference_link):link 113 | -> builder.image(**link) 114 | 115 | link = (explicit_link | reference_link | auto_link):link 116 | -> builder.reference(**link) 117 | explicit_link = label:link_label '(' sp source:url spnl title?:title sp ')' 118 | -> dict(content=link_label, uri=url, title=title) 119 | source = '<' :so '>' -> so 120 | | 121 | source_contents = (( ~'(' ~')' ~'>' nonspacechar)+ | '(' source_contents ')')* 122 | 123 | reference_link = reference_link_double | reference_link_single 124 | reference_link_double = label:label spnl ~"[]" label:description 125 | -> dict(content=label, label=description) 126 | reference_link_single = label:label (spnl "[]")? 127 | -> dict(content=label) 128 | 129 | title = ('\'' | '"'):quote 130 | <(~(exactly(quote) sp ( ')' | newline)) anything)*>:title 131 | exactly(quote) 132 | -> title 133 | 134 | auto_link = auto_link_url | auto_link_email 135 | auto_link_url = '<' ' anything)+>:url '>' 136 | -> dict(uri=url, content=url) 137 | email_special_char = '-' | '+' | '_' | '.' | '/' | '!' | '%'| '~' | '$' 138 | auto_link_email = '<' ( "mailto:" )? 139 | <(email_special_char | letterOrDigit)+ 140 | '@' ( ~newline ~'>' anything )+>:address '>' 141 | -> dict(uri='mailto:' + address, content=address) 142 | 143 | target = nonindent_space ~'[]' label:label 144 | ':' spnl ref_src:src ref_title?:title blank_line+ 145 | -> builder.target(label=label, uri=src, title=title) 146 | 147 | ref_src = 148 | 149 | ref_title = ref_title_quote | ref_title_parens 150 | ref_title_quote = spnl ('\'' | '\"'):quote 151 | <(~(exactly(quote) sp newline | newline) anything)*>:title 152 | exactly(quote) 153 | -> title 154 | ref_title_parens = spnl '(' <(~(')' sp newline | newline) anything)*>:title ')' 155 | -> title 156 | 157 | references = (target | skip_block)* 158 | 159 | code = (<'`'+ ~'`'>:t sp 160 | <((~'`' nonspacechar)+ 161 | | ~(exactly(t) ~'`') '`'+ 162 | | ~(sp exactly(t) ~'`') (spacechar | newline ~blank_line))+>:code 163 | sp exactly(t) ~'`') 164 | -> builder.literal(code) 165 | 166 | quoted = '"' (~'"' anything)* '"' | '\'' (~'\'' anything)* '\'' 167 | html_attribute = <(letterOrDigit | '-')+ spnl ('=' spnl (quoted | (~'>' nonspacechar)+))? spnl> 168 | html_comment = <"" anything)* "-->"> 169 | html_tag = <'<' spnl '/'? letter+ spnl html_attribute* '/'? spnl '>'> 170 | 171 | #TODO: Add html_block_script 172 | raw_html = (html_comment | html_tag):html 173 | -> dict(id_='html_tag', data=html) 174 | 175 | # Blocks definitions 176 | 177 | ## Block Quote 178 | 179 | quote = quote_lines+:q 180 | -> builder.quote(''.join(q)) 181 | quote_line = '>' ' '? line:quote 182 | -> quote 183 | lazy_quote_line = ~'>' ~blank_line line:quote 184 | -> quote 185 | quote_lines = quote_line:first lazy_quote_line*:rest blank_line*:blank 186 | -> first + ''.join(rest) + ('\n' if blank else '') 187 | 188 | ## Verbatim 189 | 190 | verbatim = verbatim_chunk+:chunks 191 | -> builder.verbatim(chunks) 192 | verbatim_chunk = blank_line*:blank 193 | non_blank_indented_line+:nbil 194 | -> (['\n'] * len(blank) if blank else []) + nbil 195 | 196 | 197 | ## Horizontal Rule 198 | horizontal_rule = nonindent_space 199 | (('*' sp){3} ('*' sp)* 200 | | ('-' sp){3} ('-' sp)* 201 | | ('_' sp){3} ('_' sp)*) 202 | sp newline blank_line+ 203 | -> builder.horizontal_rule() 204 | 205 | ## Headings 206 | 207 | heading = setext_heading | atx_heading 208 | 209 | atx_heading = atx_start:level sp atx_inline+:txt (sp '#'* sp)? newline 210 | -> builder.section(txt, level) 211 | atx_inline = ~newline ~(sp '#'* sp newline) inline 212 | atx_start = '#'{1,6}:x -> len(x) 213 | 214 | setext_heading = (setext_heading1 | setext_heading2):(txt, level) 215 | -> builder.section(txt, level) 216 | settext_bottom1 = '='+ newline 217 | settext_bottom2 = '-'+ newline 218 | settext_inline = (~endline inline)+:txt sp newline -> txt 219 | setext_heading1 = ~~(line settext_bottom1) settext_inline:txt settext_bottom1 -> txt, 1 220 | setext_heading2 = ~~(line settext_bottom2) settext_inline:txt settext_bottom2 -> txt, 2 221 | 222 | 223 | ## Bullet and Ordered lists 224 | 225 | bullet = ~horizontal_rule nonindent_space ('*' | '-' | '+') spacechar+ 226 | enumerator = nonindent_space digit+ '.' spacechar+ 227 | 228 | bullet_list = ~~bullet (list_tight | list_loose):list_items 229 | -> builder.bullet_list(list_items) 230 | ordered_list = ~~enumerator (list_tight | list_loose):list_items 231 | -> builder.ordered_list(list_items) 232 | 233 | 234 | list_loose = (list_item_loose:it blank_line* -> it)+:items 235 | -> items 236 | list_item_loose = (bullet | enumerator) 237 | list_block:item_block 238 | (list_continuation_block)*:continuation 239 | -> item_block + ''.join(continuation) 240 | 241 | list_tight = list_item_tight+:items blank_line* ~(bullet | enumerator) 242 | -> items 243 | list_item_tight = (bullet | enumerator) 244 | list_block:item_block 245 | (~blank_line list_continuation_block)*:continuation 246 | ~list_continuation_block 247 | -> item_block + ''.join(continuation) 248 | 249 | list_block = ~blank_line line:first list_block_line*:rest 250 | -> first + '\n'.join(rest) 251 | list_block_line = ~blank_line 252 | ~(indent? (bullet | enumerator)) 253 | ~horizontal_rule 254 | optionally_indented_line:line 255 | -> line 256 | 257 | # TODO add block separator when blankline is not empty 258 | list_continuation_block = blank_line*:blanks 259 | (indent list_block:b -> b)+:block 260 | -> ''.join(blanks) + ''.join(block) 261 | 262 | 263 | ## HTML blocks 264 | tag_name = "address" | "blockquote" | "center" | "dir" | "div" | "dl" 265 | | "fieldset" | "form" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "hr" 266 | | "isindex" | "menu" | "noframes" | "noscript" | "ol" | "p" | "pre" | "table" 267 | | "ul" | "dd" | "dt" | "frameset" | "li" | "tbody" | "td" | "tfoot" | "th" 268 | | "thead" | "tr" | "script" | "ADDRESS" | "BLOCKQUOTE" | "CENTER" | "DIR" 269 | | "DIV" | "DL" | "FIELDSET" | "FORM" | "H1" | "H2" | "H3" | "H4" | "H5" | "H6" 270 | | "HR" | "ISINDEX" | "MENU" | "NOFRAMES" | "NOSCRIPT" | "OL" | "P" | "PRE" 271 | | "TABLE" | "UL" | "DD" | "DT" | "FRAMESET" | "LI" | "TBODY" | "TD" | "TFOOT" 272 | | "TH" | "THEAD" | "TR" | "SCRIPT" 273 | 274 | #TODO: make the tags case insensitive 275 | html_block_in_tags = <'<' spnl tag_name:my_tag spnl html_attribute* '>' 276 | ( ~('<' '/' spnl exactly(my_tag) spnl '>') anything)* 277 | ~('<' '/' spnl exactly(my_tag) spnl '>')> 278 | html_block_self_closing = '<' spnl tag_name spnl html_attribute* '/' spnl '>' 279 | html_block = < ( html_block_in_tags | html_comment | html_block_self_closing ) > 280 | 281 | ## Paragraph 282 | paragraph = nonindent_space inlines:d ~~blank_line 283 | -> builder.paragraph(d) 284 | 285 | ## Being extension ready 286 | do_nothing = ~'.' '.' 287 | begin_hook = do_nothing 288 | before_verbatim = do_nothing 289 | before_horizontal_rule = do_nothing 290 | before_lists = do_nothing 291 | before_paragraph = do_nothing 292 | before_plain = do_nothing 293 | before_heading = do_nothing 294 | 295 | block = blank_line* ( 296 | begin_hook 297 | | quote 298 | | before_verbatim 299 | | verbatim 300 | | target 301 | | before_horizontal_rule 302 | | horizontal_rule 303 | | before_heading 304 | | heading 305 | | before_lists 306 | | bullet_list 307 | | ordered_list 308 | | before_paragraph 309 | | html_block 310 | | paragraph 311 | | before_plain 312 | ) 313 | 314 | document = block*:p blank_line* -> p 315 | --------------------------------------------------------------------------------