1# Python Markdown
2
3# A Python implementation of John Gruber's Markdown.
4
5# Documentation: https://python-markdown.github.io/
6# GitHub: https://github.com/Python-Markdown/markdown/
7# PyPI: https://pypi.org/project/Markdown/
8
9# Started by Manfred Stienstra (http://www.dwerg.net/).
10# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
11# Currently maintained by Waylan Limberg (https://github.com/waylan),
12# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
13
14# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
15# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
16# Copyright 2004 Manfred Stienstra (the original version)
17
18# License: BSD (see LICENSE.md for details).
19
20"""
21A block processor parses blocks of text and adds new elements to the ElementTree. Blocks of text,
22separated from other text by blank lines, may have a different syntax and produce a differently
23structured tree than other Markdown. Block processors excel at handling code formatting, equation
24layouts, tables, etc.
25"""
26
27from __future__ import annotations
28
29import logging
30import re
31import xml.etree.ElementTree as etree
32from typing import TYPE_CHECKING, Any
33from . import util
34from .blockparser import BlockParser
35
36if TYPE_CHECKING: # pragma: no cover
37 from markdown import Markdown
38
39logger = logging.getLogger('MARKDOWN')
40
41
42def build_block_parser(md: Markdown, **kwargs: Any) -> BlockParser:
43 """ Build the default block parser used by Markdown. """
44 parser = BlockParser(md)
45 parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100)
46 parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90)
47 parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80)
48 parser.blockprocessors.register(HashHeaderProcessor(parser), 'hashheader', 70)
49 parser.blockprocessors.register(SetextHeaderProcessor(parser), 'setextheader', 60)
50 parser.blockprocessors.register(HRProcessor(parser), 'hr', 50)
51 parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
52 parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
53 parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
54 parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15)
55 parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10)
56 return parser
57
58
59class BlockProcessor:
60 """ Base class for block processors.
61
62 Each subclass will provide the methods below to work with the source and
63 tree. Each processor will need to define it's own `test` and `run`
64 methods. The `test` method should return True or False, to indicate
65 whether the current block should be processed by this processor. If the
66 test passes, the parser will call the processors `run` method.
67
68 Attributes:
69 BlockProcessor.parser (BlockParser): The `BlockParser` instance this is attached to.
70 BlockProcessor.tab_length (int): The tab length set on the `Markdown` instance.
71
72 """
73
74 def __init__(self, parser: BlockParser):
75 self.parser = parser
76 self.tab_length = parser.md.tab_length
77
78 def lastChild(self, parent: etree.Element) -> etree.Element | None:
79 """ Return the last child of an `etree` element. """
80 if len(parent):
81 return parent[-1]
82 else:
83 return None
84
85 def detab(self, text: str, length: int | None = None) -> tuple[str, str]:
86 """ Remove a tab from the front of each line of the given text. """
87 if length is None:
88 length = self.tab_length
89 newtext = []
90 lines = text.split('\n')
91 for line in lines:
92 if line.startswith(' ' * length):
93 newtext.append(line[length:])
94 elif not line.strip():
95 newtext.append('')
96 else:
97 break
98 return '\n'.join(newtext), '\n'.join(lines[len(newtext):])
99
100 def looseDetab(self, text: str, level: int = 1) -> str:
101 """ Remove a tab from front of lines but allowing dedented lines. """
102 lines = text.split('\n')
103 for i in range(len(lines)):
104 if lines[i].startswith(' '*self.tab_length*level):
105 lines[i] = lines[i][self.tab_length*level:]
106 return '\n'.join(lines)
107
108 def test(self, parent: etree.Element, block: str) -> bool:
109 """ Test for block type. Must be overridden by subclasses.
110
111 As the parser loops through processors, it will call the `test`
112 method on each to determine if the given block of text is of that
113 type. This method must return a boolean `True` or `False`. The
114 actual method of testing is left to the needs of that particular
115 block type. It could be as simple as `block.startswith(some_string)`
116 or a complex regular expression. As the block type may be different
117 depending on the parent of the block (i.e. inside a list), the parent
118 `etree` element is also provided and may be used as part of the test.
119
120 Keyword arguments:
121 parent: An `etree` element which will be the parent of the block.
122 block: A block of text from the source which has been split at blank lines.
123 """
124 pass # pragma: no cover
125
126 def run(self, parent: etree.Element, blocks: list[str]) -> bool | None:
127 """ Run processor. Must be overridden by subclasses.
128
129 When the parser determines the appropriate type of a block, the parser
130 will call the corresponding processor's `run` method. This method
131 should parse the individual lines of the block and append them to
132 the `etree`.
133
134 Note that both the `parent` and `etree` keywords are pointers
135 to instances of the objects which should be edited in place. Each
136 processor must make changes to the existing objects as there is no
137 mechanism to return new/different objects to replace them.
138
139 This means that this method should be adding `SubElements` or adding text
140 to the parent, and should remove (`pop`) or add (`insert`) items to
141 the list of blocks.
142
143 If `False` is returned, this will have the same effect as returning `False`
144 from the `test` method.
145
146 Keyword arguments:
147 parent: An `etree` element which is the parent of the current block.
148 blocks: A list of all remaining blocks of the document.
149 """
150 pass # pragma: no cover
151
152
153class ListIndentProcessor(BlockProcessor):
154 """ Process children of list items.
155
156 Example
157
158 * a list item
159 process this part
160
161 or this part
162
163 """
164
165 ITEM_TYPES = ['li']
166 """ List of tags used for list items. """
167 LIST_TYPES = ['ul', 'ol']
168 """ Types of lists this processor can operate on. """
169
170 def __init__(self, *args):
171 super().__init__(*args)
172 self.INDENT_RE = re.compile(r'^(([ ]{%s})+)' % self.tab_length)
173
174 def test(self, parent: etree.Element, block: str) -> bool:
175 # A list calls `parseBlocks` once per level of nesting, so the
176 # nesting has to stop short of the interpreter's limit. The markers which no
177 # longer nest are kept as text, as they are for a blockquote.
178 return block.startswith(' '*self.tab_length) and \
179 not self.parser.state.isstate('detabbed') and \
180 not util.nearing_recursion_limit() and \
181 (parent.tag in self.ITEM_TYPES or
182 (len(parent) and parent[-1] is not None and
183 (parent[-1].tag in self.LIST_TYPES)))
184
185 def run(self, parent: etree.Element, blocks: list[str]) -> None:
186 block = blocks.pop(0)
187 level, sibling = self.get_level(parent, block)
188 block = self.looseDetab(block, level)
189
190 self.parser.state.set('detabbed')
191 if parent.tag in self.ITEM_TYPES:
192 # It's possible that this parent has a `ul` or `ol` child list
193 # with a member. If that is the case, then that should be the
194 # parent. This is intended to catch the edge case of an indented
195 # list whose first member was parsed previous to this point
196 # see `OListProcessor`
197 if len(parent) and parent[-1].tag in self.LIST_TYPES:
198 self.parser.parseBlocks(parent[-1], [block])
199 else:
200 # The parent is already a `li`. Just parse the child block.
201 self.parser.parseBlocks(parent, [block])
202 elif sibling.tag in self.ITEM_TYPES:
203 # The sibling is a `li`. Use it as parent.
204 self.parser.parseBlocks(sibling, [block])
205 elif len(sibling) and sibling[-1].tag in self.ITEM_TYPES:
206 # The parent is a list (`ol` or `ul`) which has children.
207 # Assume the last child `li` is the parent of this block.
208 if sibling[-1].text:
209 # If the parent `li` has text, that text needs to be moved to a `p`
210 # The `p` must be 'inserted' at beginning of list in the event
211 # that other children already exist i.e.; a nested sub-list.
212 p = etree.Element('p')
213 p.text = sibling[-1].text
214 sibling[-1].text = ''
215 sibling[-1].insert(0, p)
216 self.parser.parseChunk(sibling[-1], block)
217 else:
218 self.create_item(sibling, block)
219 self.parser.state.reset()
220
221 def create_item(self, parent: etree.Element, block: str) -> None:
222 """ Create a new `li` and parse the block with it as the parent. """
223 li = etree.SubElement(parent, 'li')
224 self.parser.parseBlocks(li, [block])
225
226 def get_level(self, parent: etree.Element, block: str) -> tuple[int, etree.Element]:
227 """ Get level of indentation based on list level. """
228 # Get indent level
229 m = self.INDENT_RE.match(block)
230 if m:
231 indent_level = len(m.group(1))/self.tab_length
232 else:
233 indent_level = 0
234 if self.parser.state.isstate('list'):
235 # We're in a tight-list - so we already are at correct parent.
236 level = 1
237 else:
238 # We're in a loose-list - so we need to find parent.
239 level = 0
240 # Step through children of tree to find matching indent level.
241 while indent_level > level:
242 child = self.lastChild(parent)
243 if (child is not None and
244 (child.tag in self.LIST_TYPES or child.tag in self.ITEM_TYPES)):
245 if child.tag in self.LIST_TYPES:
246 level += 1
247 parent = child
248 else:
249 # No more child levels. If we're short of `indent_level`,
250 # we have a code block. So we stop here.
251 break
252 return level, parent
253
254
255class CodeBlockProcessor(BlockProcessor):
256 """ Process code blocks. """
257
258 def test(self, parent: etree.Element, block: str) -> bool:
259 return block.startswith(' '*self.tab_length)
260
261 def run(self, parent: etree.Element, blocks: list[str]) -> None:
262 sibling = self.lastChild(parent)
263 block = blocks.pop(0)
264 theRest = ''
265 if (sibling is not None and sibling.tag == "pre" and
266 len(sibling) and sibling[0].tag == "code"):
267 # The previous block was a code block. As blank lines do not start
268 # new code blocks, append this block to the previous, adding back
269 # line breaks removed from the split into a list.
270 code = sibling[0]
271 block, theRest = self.detab(block)
272 code.text = util.AtomicString(
273 '{}\n{}\n'.format(code.text, util.code_escape(block.rstrip()))
274 )
275 else:
276 # This is a new code block. Create the elements and insert text.
277 pre = etree.SubElement(parent, 'pre')
278 code = etree.SubElement(pre, 'code')
279 block, theRest = self.detab(block)
280 code.text = util.AtomicString('%s\n' % util.code_escape(block.rstrip()))
281 if theRest:
282 # This block contained unindented line(s) after the first indented
283 # line. Insert these lines as the first block of the master blocks
284 # list for future processing.
285 blocks.insert(0, theRest)
286
287
288class BlockQuoteProcessor(BlockProcessor):
289 """ Process blockquotes. """
290
291 RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)')
292
293 def test(self, parent: etree.Element, block: str) -> bool:
294 return bool(self.RE.search(block)) and not util.nearing_recursion_limit()
295
296 def run(self, parent: etree.Element, blocks: list[str]) -> None:
297 block = blocks.pop(0)
298 m = self.RE.search(block)
299 if m:
300 before = block[:m.start()] # Lines before blockquote
301 # Pass lines before blockquote in recursively for parsing first.
302 self.parser.parseBlocks(parent, [before])
303 # Remove `> ` from beginning of each line.
304 block = '\n'.join(
305 [self.clean(line) for line in block[m.start():].split('\n')]
306 )
307 sibling = self.lastChild(parent)
308 if sibling is not None and sibling.tag == "blockquote":
309 # Previous block was a blockquote so set that as this blocks parent
310 quote = sibling
311 else:
312 # This is a new blockquote. Create a new parent element.
313 quote = etree.SubElement(parent, 'blockquote')
314 # Recursively parse block with blockquote as parent.
315 # change parser state so blockquotes embedded in lists use `p` tags
316 self.parser.state.set('blockquote')
317 self.parser.parseChunk(quote, block)
318 self.parser.state.reset()
319
320 def clean(self, line: str) -> str:
321 """ Remove `>` from beginning of a line. """
322 m = self.RE.match(line)
323 if line.strip() == ">":
324 return ""
325 elif m:
326 return m.group(2)
327 else:
328 return line
329
330
331class OListProcessor(BlockProcessor):
332 """ Process ordered list blocks. """
333
334 TAG: str = 'ol'
335 """ The tag used for the the wrapping element. """
336 STARTSWITH: str = '1'
337 """
338 The integer (as a string ) with which the list starts. For example, if a list is initialized as
339 `3. Item`, then the `ol` tag will be assigned an HTML attribute of `starts="3"`. Default: `"1"`.
340 """
341 LAZY_OL: bool = True
342 """ Ignore `STARTSWITH` if `True`. """
343 SIBLING_TAGS: list[str] = ['ol', 'ul']
344 """
345 Markdown does not require the type of a new list item match the previous list item type.
346 This is the list of types which can be mixed.
347 """
348
349 def __init__(self, parser: BlockParser):
350 super().__init__(parser)
351 # Detect an item (`1. item`). `group(1)` contains contents of item.
352 self.RE = re.compile(r'^[ ]{0,%d}\d+\.[ ]+(.*)' % (self.tab_length - 1))
353 # Detect items on secondary lines. they can be of either list type.
354 self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.)|[*+-])[ ]+(.*)' %
355 (self.tab_length - 1))
356 # Detect indented (nested) items of either type
357 self.INDENT_RE = re.compile(r'^[ ]{%d,%d}((\d+\.)|[*+-])[ ]+.*' %
358 (self.tab_length, self.tab_length * 2 - 1))
359
360 def test(self, parent: etree.Element, block: str) -> bool:
361 # As in `ListIndentProcessor`, nesting stops short of the recursion limit.
362 # `UListProcessor` inherits this method, so unordered lists are covered too.
363 return bool(self.RE.match(block)) and not util.nearing_recursion_limit()
364
365 def run(self, parent: etree.Element, blocks: list[str]) -> None:
366 # Check for multiple items in one block.
367 items = self.get_items(blocks.pop(0))
368 sibling = self.lastChild(parent)
369
370 if sibling is not None and sibling.tag in self.SIBLING_TAGS:
371 # Previous block was a list item, so set that as parent
372 lst = sibling
373 # make sure previous item is in a `p` - if the item has text,
374 # then it isn't in a `p`
375 if lst[-1].text:
376 # since it's possible there are other children for this
377 # sibling, we can't just `SubElement` the `p`, we need to
378 # insert it as the first item.
379 p = etree.Element('p')
380 p.text = lst[-1].text
381 lst[-1].text = ''
382 lst[-1].insert(0, p)
383 # if the last item has a tail, then the tail needs to be put in a `p`
384 # likely only when a header is not followed by a blank line
385 lch = self.lastChild(lst[-1])
386 if lch is not None and lch.tail:
387 p = etree.SubElement(lst[-1], 'p')
388 p.text = lch.tail.lstrip()
389 lch.tail = ''
390
391 # parse first block differently as it gets wrapped in a `p`.
392 li = etree.SubElement(lst, 'li')
393 self.parser.state.set('looselist')
394 firstitem = items.pop(0)
395 self.parser.parseBlocks(li, [firstitem])
396 self.parser.state.reset()
397 elif parent.tag in ['ol', 'ul']:
398 # this catches the edge case of a multi-item indented list whose
399 # first item is in a blank parent-list item:
400 # * * subitem1
401 # * subitem2
402 # see also `ListIndentProcessor`
403 lst = parent
404 else:
405 # This is a new list so create parent with appropriate tag.
406 lst = etree.SubElement(parent, self.TAG)
407 # Check if a custom start integer is set
408 if not self.LAZY_OL and self.STARTSWITH != '1':
409 lst.attrib['start'] = self.STARTSWITH
410
411 self.parser.state.set('list')
412 # Loop through items in block, recursively parsing each with the
413 # appropriate parent.
414 for item in items:
415 if item.startswith(' '*self.tab_length):
416 # Item is indented. Parse with last item as parent
417 self.parser.parseBlocks(lst[-1], [item])
418 else:
419 # New item. Create `li` and parse with it as parent
420 li = etree.SubElement(lst, 'li')
421 self.parser.parseBlocks(li, [item])
422 self.parser.state.reset()
423
424 def get_items(self, block: str) -> list[str]:
425 """ Break a block into list items. """
426 # The lines of an item are collected in a list and joined once at the end.
427 # Appending per line, as in `item = '{}\n{}'.format(item, line)`, copies the
428 # whole item every time, which is quadratic in the number of lines of an item.
429 items: list[list[str]] = []
430 for line in block.split('\n'):
431 m = self.CHILD_RE.match(line)
432 if m:
433 # This is a new list item
434 # Check first item for the start index
435 if not items and self.TAG == 'ol':
436 # Detect the integer value of first list item
437 INTEGER_RE = re.compile(r'(\d+)')
438 self.STARTSWITH = INTEGER_RE.match(m.group(1)).group()
439 # Append to the list
440 items.append([m.group(3)])
441 elif self.INDENT_RE.match(line):
442 # This is an indented (possibly nested) item.
443 if items[-1][0].startswith(' '*self.tab_length):
444 # Previous item was indented. Append to that item.
445 items[-1].append(line)
446 else:
447 items.append([line])
448 else:
449 # This is another line of previous item. Append to that item.
450 items[-1].append(line)
451 return ['\n'.join(item) for item in items]
452
453
454class UListProcessor(OListProcessor):
455 """ Process unordered list blocks. """
456
457 TAG: str = 'ul'
458 """ The tag used for the the wrapping element. """
459
460 def __init__(self, parser: BlockParser):
461 super().__init__(parser)
462 # Detect an item (`1. item`). `group(1)` contains contents of item.
463 self.RE = re.compile(r'^[ ]{0,%d}[*+-][ ]+(.*)' % (self.tab_length - 1))
464
465
466class HashHeaderProcessor(BlockProcessor):
467 """ Process Hash Headers. """
468
469 # Detect a header at start of any line in block
470 RE = re.compile(r'(?:^|\n)(?P<level>#{1,6})(?P<header>(?:\\.|[^\\#]|#+(?=[^#\n]))*?)#*(?:\n|$)')
471
472 def test(self, parent: etree.Element, block: str) -> bool:
473 # Check for a line starting with `#` before running the regex, which is
474 # slow to scan long blocks.
475 return (block.startswith('#') or '\n#' in block) and bool(self.RE.search(block))
476
477 def run(self, parent: etree.Element, blocks: list[str]) -> None:
478 block = blocks.pop(0)
479 m = self.RE.search(block)
480 if m:
481 before = block[:m.start()] # All lines before header
482 after = block[m.end():] # All lines after header
483 if before:
484 # As the header was not the first line of the block and the
485 # lines before the header must be parsed first,
486 # recursively parse this lines as a block.
487 self.parser.parseBlocks(parent, [before])
488 # Create header using named groups from RE
489 h = etree.SubElement(parent, 'h%d' % len(m.group('level')))
490 h.text = m.group('header').strip()
491 if after:
492 # Insert remaining lines as first block for future parsing.
493 if self.parser.state.isstate('looselist'):
494 # This is a weird edge case where a header is a child of a loose list
495 # and there is no blank line after the header. To ensure proper
496 # parsing, the line(s) after need to be detabbed. See #1443.
497 after = self.looseDetab(after)
498 blocks.insert(0, after)
499 else: # pragma: no cover
500 # This should never happen, but just in case...
501 logger.warn("We've got a problem header: %r" % block)
502
503
504class SetextHeaderProcessor(BlockProcessor):
505 """ Process Setext-style Headers. """
506
507 # Detect Setext-style header. Must be first 2 lines of block.
508 RE = re.compile(r'^.*?\n(?:=+|-+)[ ]*(\n|$)', re.MULTILINE)
509
510 def test(self, parent: etree.Element, block: str) -> bool:
511 return bool(self.RE.match(block))
512
513 def run(self, parent: etree.Element, blocks: list[str]) -> None:
514 lines = blocks.pop(0).split('\n', 2)
515 # Determine level. `=` is 1 and `-` is 2.
516 if lines[1].startswith('='):
517 level = 1
518 else:
519 level = 2
520 h = etree.SubElement(parent, 'h%d' % level)
521 h.text = lines[0].strip()
522 if len(lines) > 2:
523 # Block contains additional lines. Add to master blocks for later.
524 blocks.insert(0, lines[2])
525
526
527class HRProcessor(BlockProcessor):
528 """ Process Horizontal Rules. """
529
530 # Python's `re` module doesn't officially support atomic grouping. However you can fake it.
531 # See https://stackoverflow.com/a/13577411/866026
532 RE = r'^[ ]{0,3}(?=(?P<atomicgroup>(-+[ ]{0,2}){3,}|(_+[ ]{0,2}){3,}|(\*+[ ]{0,2}){3,}))(?P=atomicgroup)[ ]*$'
533 # Detect hr on any line of a block.
534 SEARCH_RE = re.compile(RE, re.MULTILINE)
535
536 def test(self, parent: etree.Element, block: str) -> bool:
537 m = self.SEARCH_RE.search(block)
538 if m:
539 # Save match object on class instance so we can use it later.
540 self.match = m
541 return True
542 return False
543
544 def run(self, parent: etree.Element, blocks: list[str]) -> None:
545 block = blocks.pop(0)
546 match = self.match
547 # Check for lines in block before `hr`.
548 prelines = block[:match.start()].rstrip('\n')
549 if prelines:
550 # Recursively parse lines before `hr` so they get parsed first.
551 self.parser.parseBlocks(parent, [prelines])
552 # create hr
553 etree.SubElement(parent, 'hr')
554 # check for lines in block after `hr`.
555 postlines = block[match.end():].lstrip('\n')
556 if postlines:
557 # Add lines after `hr` to master blocks for later parsing.
558 blocks.insert(0, postlines)
559
560
561class EmptyBlockProcessor(BlockProcessor):
562 """ Process blocks that are empty or start with an empty line. """
563
564 def test(self, parent: etree.Element, block: str) -> bool:
565 return not block or block.startswith('\n')
566
567 def run(self, parent: etree.Element, blocks: list[str]) -> None:
568 block = blocks.pop(0)
569 filler = '\n\n'
570 if block:
571 # Starts with empty line
572 # Only replace a single line.
573 filler = '\n'
574 # Save the rest for later.
575 theRest = block[1:]
576 if theRest:
577 # Add remaining lines to master blocks for later.
578 blocks.insert(0, theRest)
579 sibling = self.lastChild(parent)
580 if (sibling is not None and sibling.tag == 'pre' and
581 len(sibling) and sibling[0].tag == 'code'):
582 # Last block is a code block. Append to preserve whitespace.
583 sibling[0].text = util.AtomicString(
584 '{}{}'.format(sibling[0].text, filler)
585 )
586
587
588class ReferenceProcessor(BlockProcessor):
589 """ Process link references. """
590 RE = re.compile(
591 r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*(?:\n[ ]*)?([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$',
592 re.MULTILINE
593 )
594
595 def test(self, parent: etree.Element, block: str) -> bool:
596 return True
597
598 def run(self, parent: etree.Element, blocks: list[str]) -> bool:
599 block = blocks.pop(0)
600 m = self.RE.search(block)
601 if m:
602 start = end = m.start()
603 # Record the definitions on the following lines in this pass rather
604 # than one per pass through the block processors. A definition after
605 # the first which spans lines is left for the next pass, as another
606 # processor may claim part of it.
607 for m in self.RE.finditer(block, start):
608 if m.start() > start and (block[end:m.start()].strip('\n') or '\n' in m.group()):
609 break
610 id = m.group(1).strip().lower()
611 link = m.group(2).lstrip('<').rstrip('>')
612 title = m.group(5) or m.group(6)
613 self.parser.md.references[id] = (link, title)
614 end = m.end()
615 if block[end:].strip():
616 # Add any content after match back to blocks as separate block
617 blocks.insert(0, block[end:].lstrip('\n'))
618 if block[:start].strip():
619 # Add any content before match back to blocks as separate block
620 blocks.insert(0, block[:start].rstrip('\n'))
621 return True
622 # No match. Restore block.
623 blocks.insert(0, block)
624 return False
625
626
627class ParagraphProcessor(BlockProcessor):
628 """ Process Paragraph blocks. """
629
630 def test(self, parent: etree.Element, block: str) -> bool:
631 return True
632
633 def run(self, parent: etree.Element, blocks: list[str]) -> None:
634 block = blocks.pop(0)
635 if block.strip():
636 # Not a blank block. Add to parent, otherwise throw it away.
637 if self.parser.state.isstate('list'):
638 # The parent is a tight-list.
639 #
640 # Check for any children. This will likely only happen in a
641 # tight-list when a header isn't followed by a blank line.
642 # For example:
643 #
644 # * # Header
645 # Line 2 of list item - not part of header.
646 sibling = self.lastChild(parent)
647 if sibling is not None:
648 # Insert after sibling.
649 if sibling.tail:
650 sibling.tail = '{}\n{}'.format(sibling.tail, block)
651 else:
652 sibling.tail = '\n%s' % block
653 else:
654 # Append to parent.text
655 if parent.text:
656 parent.text = '{}\n{}'.format(parent.text, block)
657 else:
658 parent.text = block.lstrip()
659 else:
660 # Create a regular paragraph
661 p = etree.SubElement(parent, 'p')
662 p.text = block.lstrip()