diff --git a/docs/changelog.md b/docs/changelog.md index 7926ab4aa..81368a191 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,6 +14,28 @@ See the [Contributing Guide](contributing.md) for details. ## Unreleased +### Added + +* Add the `finish` and `end_blocks` methods to `BlockProcessor`, which the + block parser calls when its outermost call is done and when a nested call is + done. A block processor which holds on to blocks until a later call to the + parser must release them there. Content held for a nested block, such as an + unclosed raw HTML block in a list item, may be added to it after the call to + `parseChunk` which parsed it has returned. + +### Changed + +* Recognize raw HTML blocks nested in a list item, definition, blockquote, + admonition or footnote as at the top level of the document, rather than + parsing them as Markdown and splitting them into paragraphs at blank lines + (#1096). As at the top level, Markdown is no longer parsed within such a + block, unless the `md_in_html` extension is enabled and the element has a + `markdown` attribute, which is now supported in those places too. Unlike at + the top level, a nested raw HTML block must start a block of its own and + cannot interrupt a paragraph, and an unclosed tag runs to the end of the + nesting block rather than of the document. See + [Raw HTML Blocks](index.md#differences) for details. + ### Fixed * Fix for complex cases in new emphasis handling where text could be unexpectedly diff --git a/docs/extensions/api.md b/docs/extensions/api.md index 4822bbc2b..3fc2c91a6 100644 --- a/docs/extensions/api.md +++ b/docs/extensions/api.md @@ -114,6 +114,33 @@ processor. There are three methods: For perspective, Markdown calls `parseDocument` which calls `parseChunk` which calls `parseBlocks` which calls your block processor, which, in turn, might call one of these routines. +#### Holding Blocks Across Calls + +A block which contains other blocks, such as a list item or an admonition, hands its content to the parser a block at a +time, in a call for each. A block processor which needs to see a later block before it knows what to do with an +earlier one may hold on to blocks across those calls, but must release them once no more are coming. The built-in +processor for raw HTML blocks nested in other blocks does this for a raw HTML block with a blank line in it. Two +methods of `BlockProcessor`, which do nothing unless overridden, are called for that: + +* [`end_blocks`][markdown.blockprocessors.BlockProcessor.end_blocks] is called with `parent` when a nested call to + the parser, which parsed blocks into `parent`, is done. The caller may do as it likes with `parent` once the call + returns, so anything held which no call still in progress can be counted on to hand more blocks to must be + released here. +* [`finish`][markdown.blockprocessors.BlockProcessor.finish] is called when the outermost call to the parser is done. + Anything still held must be released here. + +This affects a block processor which calls the parser itself. If it parses into an element of its own, which is not in +the tree being parsed (to move what is parsed somewhere else, for example), everything parsed is in that element by +the time the call returns. If it parses into an element at the end of the tree being parsed, which a later call may +hand more blocks to (as a list item is), what is held for that element may be added to it after the call returns: once +a later block closes it, or once the outermost call is done. + +!!! warning + + The parser calls these methods from [`parseBlocks`][markdown.blockparser.BlockParser.parseBlocks]. A subclass of + `BlockParser` which overrides `parseBlocks` must call the parent class's `parseBlocks` to parse the blocks. + Otherwise, blocks held by a block processor are never released, and their content is lost from the output. + #### Example This example calls out important paragraphs by giving them a border. It looks for a fence line of exclamation points @@ -198,6 +225,7 @@ Some block processors in the Markdown source tree include: | [`HashHeaderProcessor`][markdown.blockprocessors.HashHeaderProcessor] | built-in | Title hashes (`#`), which may split blocks | | [`HRProcessor`][markdown.blockprocessors.HRProcessor] | built-in | Horizontal lines, e.g., `---` | | [`OListProcessor`][markdown.blockprocessors.OListProcessor] | built-in | Ordered lists; complex and using `state` | +| [`RawHtmlBlockProcessor`][markdown.blockprocessors.RawHtmlBlockProcessor] | built-in | Nested raw HTML; holds blocks across calls | | [`AdmonitionProcessor`][markdown.extensions.admonition.AdmonitionProcessor] | extension | Render each [Admonition][] in a new `div` | | [`FootnoteBlockProcessor`][markdown.extensions.footnotes.FootnoteBlockProcessor] | extension | Find footnote definitions and store for later use. | diff --git a/docs/extensions/md_in_html.md b/docs/extensions/md_in_html.md index 13a9a88b3..224ab70cd 100644 --- a/docs/extensions/md_in_html.md +++ b/docs/extensions/md_in_html.md @@ -170,6 +170,27 @@ behavior. However, if the value of an element's `markdown` attribute is the same parent, the the child element's behavior is observed. For example, a `block` element may contain either `block` elements or `span` elements as children and each element will be parsed using the specified behavior. +### In Lists and Other Blocks + +A raw HTML block nested in a list item, definition, blockquote, admonition or footnote may have a `markdown` +attribute as well. Its content is parsed as it would be at the top level of the document, whatever block it is nested +in. For example, a paragraph within it is wrapped in a `

` tag even in a tight list item, whose own text is not. + +``` md-render +--- +extensions: [md_in_html] +--- +*

This is a *Markdown* paragraph.
+* A tight list item. +* Another list item. + +
+ This is a *Markdown* paragraph too. +
+``` + +See [Raw HTML Blocks](../index.md#differences) for where a raw HTML block is recognized in nested content. + ### Tag Normalization While the default behavior is for Markdown to not alter raw HTML, as this extension is parsing the content of raw HTML elements, it will do some normalization of the tags of block-level elements. For example: diff --git a/docs/index.md b/docs/index.md index 7912d2361..357b7ab56 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,6 +118,35 @@ are summarized below: foreseeable future. That said, the [Sane List Extension](extensions/sane_lists.md) is available to provide a less surprising behavior. +* __Raw HTML Blocks__ + + The [syntax rules](https://daringfireball.net/projects/markdown/syntax#html) + state that a block-level HTML element must be separated from surrounding + content by blank lines, and that its start and end tags should not be + indented. At the top level of a document, Python-Markdown is more lenient: a + line which starts with a block-level tag, or with a comment, processing + instruction, `CDATA` section or declaration, indented by no more than three + spaces, starts a raw HTML block, even when it directly follows a line of a + paragraph. The block continues, across blank lines, until its tags are closed, + and a tag which is never closed runs to the end of the document. Markdown is + not parsed within a raw HTML block (the + [Markdown in HTML Extension](extensions/md_in_html.md) can change that). + + Nested in a list item, definition, blockquote, admonition or footnote, a raw + HTML block follows the same rules once the indentation of the nesting (or the + `>` of a blockquote) is removed, with two differences: + + 1. A raw HTML block must start a block of its own: after a blank line, right + after a list marker or the `:` of a definition, or on the first line of + the nested content. A block-level tag on a later line of a paragraph, + including a lazy continuation line, remains part of that paragraph. A + fenced code block is not recognized in nested content by the + [Fenced Code Blocks Extension](extensions/fenced_code_blocks.md), so + without this rule, the HTML of a code example in a list item would be + rendered rather than shown. + 2. A tag which is never closed runs to the end of the list item (or other + nesting block) rather than to the end of the document. + * __Automatic Links__ Automatic links recognize URL schemes followed by `://`, including schemes diff --git a/markdown/blockparser.py b/markdown/blockparser.py index 549c9ecd5..60a1c621d 100644 --- a/markdown/blockparser.py +++ b/markdown/blockparser.py @@ -96,6 +96,8 @@ def __init__(self, md: Markdown): self.blockprocessors: util.Registry[BlockProcessor] = util.Registry() self.state = State() self.md = md + # The parent of each call to `parseBlocks` in progress, outermost first. + self._frames: list[etree.Element] = [] def parseDocument(self, lines: Iterable[str]) -> etree.ElementTree: """ Parse a Markdown document into an `ElementTree`. @@ -113,6 +115,7 @@ def parseDocument(self, lines: Iterable[str]) -> etree.ElementTree: An element tree. """ # Create an `ElementTree` from the lines + self._frames = [] self.root = etree.Element(self.md.doc_tag) self.parseChunk(self.root, '\n'.join(lines)) return etree.ElementTree(self.root) @@ -147,14 +150,33 @@ def parseBlocks(self, parent: etree.Element, blocks: list[str]) -> None: additional `BlockProcessors` which call this method to recursively parse a nested block. + Once the blocks are parsed, this calls each processor's + [`end_blocks`][markdown.blockprocessors.BlockProcessor.end_blocks] (for a + nested call) or [`finish`][markdown.blockprocessors.BlockProcessor.finish] + (for the outermost call), so that a processor can release blocks it holds. + A subclass which overrides this method must call it to parse the blocks; + otherwise, held blocks are never released and their content is lost. + Arguments: parent: The parent element. blocks: The blocks of text to parse. """ - while blocks: + self._frames.append(parent) + try: + while blocks: + for processor in self.blockprocessors: + if processor.test(parent, blocks[0]): + if processor.run(parent, blocks) is not False: + # run returns True or None + break + finally: + self._frames.pop() + if self._frames: + # The caller may do as it likes with `parent` from here on. + for processor in self.blockprocessors: + processor.end_blocks(parent) + else: + # The outermost call is done, so nothing more will be handed to a processor for these parents. for processor in self.blockprocessors: - if processor.test(parent, blocks[0]): - if processor.run(parent, blocks) is not False: - # run returns True or None - break + processor.finish() diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index 4465ce472..9e1d63459 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -43,6 +43,7 @@ def build_block_parser(md: Markdown, **kwargs: Any) -> BlockParser: """ Build the default block parser used by Markdown. """ parser = BlockParser(md) parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100) + parser.blockprocessors.register(RawHtmlBlockProcessor(parser), 'rawhtml', 95) parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90) parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80) parser.blockprocessors.register(HashHeaderProcessor(parser), 'hashheader', 70) @@ -149,6 +150,239 @@ def run(self, parent: etree.Element, blocks: list[str]) -> bool | None: """ pass # pragma: no cover + def finish(self) -> None: + """ Called when the outermost call to the parser is done. Does nothing unless overridden. + + A processor which holds on to blocks, to decide what to do with them once it has seen the next, must + release them here: this is the last the parser will hand it for the parents involved. + """ + pass + + def end_blocks(self, parent: etree.Element) -> None: + """ Called when a nested call to the parser that parsed blocks into `parent` is done. Does nothing unless + overridden. + + The caller may do as it likes with `parent` and what was parsed into it once the call returns. A processor + which holds on to blocks until a later call, and can not be sure there will be one, must release them here. + """ + pass + + +class HeldBlocks(list): + """ + Blocks which are held back until it is known whether they are raw HTML, the parser state they are in, and what + ends the construct they start. + """ + + def __init__(self, blocks: list[str], state: list, terminator: re.Pattern[str]): + super().__init__(blocks) + self.state = list(state) + self.terminator = terminator + + +class RawHtmlBlockProcessor(BlockProcessor): + """ + Extract raw HTML blocks nested in a container. + + The preprocessor can only extract a raw block that is at the start of a line. Within a container (a list item, + an admonition, a blockquote, etc.) the block is indented or quoted, which can not be told apart from an indented + code block or a paragraph until the container has removed its indentation or markers. At that point the block + starts a block of text and is extracted here. + + Containers pass their content on a block at a time, so a raw block with blank lines in it is collected across + calls until it is closed. Anything still open when the parser is done is handled in + [`finish`][markdown.blockprocessors.RawHtmlBlockProcessor.finish]. + """ + + START_RE = re.compile(r'^[ ]{0,3}(?: + + after + ''' + ), + self.dedent( + ''' +
+

Title

+ +

after

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_admonition_unclosed_div(self): + """ + Unclosed raw block at end of document (flush in close()). Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" +
+ **test** + ''' + ), + self.dedent( + ''' +
+

Title

+
+ **test** +
+ ''' + ), + extensions=['admonition'] + ) + + def test_paragraph_continuation_div(self): + """ + An indented line directly after paragraph text is a paragraph continuation, not code. Regression: + renders as it always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
x
+ ''' + ), + self.dedent( + ''' +

some text +

x

+ ''' + ) + ) + + def test_paragraph_continuation_div_multiline(self): + """Same, multi-line raw block. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
+ **x** +
+ more text + ''' + ), + self.dedent( + ''' +

some text +

+ x +
+ more text

+ ''' + ) + ) + + def test_paragraph_chain_with_text(self): + """ + Blocks that start where the last one ended, mid-paragraph, then text. Must stay one inline run. + Regression: renders as it always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
a
b
trailing + ''' + ), + self.dedent( + ''' +

some text +

a
b
trailing

+ ''' + ) + ) + + def test_paragraph_chain_with_more_lines(self): + """ + Same with an indented continuation line and a following paragraph. Regression: renders as it always + has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
a
b
trailing + more + + after + ''' + ), + self.dedent( + ''' +

some text +

a
b
trailing + more

+

after

+ ''' + ) + ) + + def test_paragraph_chain_of_three(self): + """Three blocks in a row. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
a
b
c
+ ''' + ), + self.dedent( + ''' +

some text +

a
b
c

+ ''' + ) + ) + + def test_paragraph_chain_multiline(self): + """ Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
+ a +
+ b +
trailing + more + ''' + ), + self.dedent( + ''' +

some text +

+ a +
+ b +
trailing + more

+ ''' + ) + ) + + def test_paragraph_chain_multiline_indented(self): + """Same with the continuation line indented. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
+ a +
+ b +
trailing + more + ''' + ), + self.dedent( + ''' +

some text +

+ a +
+ b +
trailing + more

+ ''' + ) + ) + + def test_code_chain_blocks(self): + """ Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + text + +
a
b
c
trailing + more + ''' + ), + self.dedent( + ''' +

text

+
<div>a</div><div>b</div><div>c</div> trailing
+                more
+                
+ ''' + ) + ) + + def test_code_chain_multiline(self): + """Same, multi-line blocks. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + text + +
+ a +
+ b +
trailing + more + ''' + ), + self.dedent( + ''' +

text

+
<div>
+                a
+                </div><div>
+                b
+                </div> trailing
+                more
+                
+ ''' + ) + ) + + def test_list_lazy_chain(self): + """ Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + - item +
a
b
trailing + ''' + ), + self.dedent( + ''' + + ''' + ) + ) + + def test_list_lazy_div_next_item(self): + """ + Indented div on a lazy line of a list item, followed by another item. Both items must stay tight (no +

): nothing may be inserted between the placeholder and the next item. Regression: renders as it + always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + - item +

x
+ - b + ''' + ), + self.dedent( + ''' + + ''' + ) + ) + + def test_list_lazy_tail_next_item(self): + """ + Same with text after the closing tag. The next item must not be swallowed into the paragraph. + Regression: renders as it always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + - item +
x
trailing + - b + ''' + ), + self.dedent( + ''' + + ''' + ) + ) + + def test_list_lazy_tail_next_item_ordered(self): + """Same in an ordered list. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + 1. item +
x
trailing + 2. b + ''' + ), + self.dedent( + ''' +
    +
  1. item +
    x
    trailing
  2. +
  3. b
  4. +
+ ''' + ) + ) + + def test_paragraph_tail_then_code(self): + """ + Text after the closing tag in a paragraph, then a blank line and an indented code block. The code + block must stay a code block. Regression: renders as it always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + some text +
x
trailing + + code + ''' + ), + self.dedent( + ''' +

some text +

x
trailing

+
code
+                
+ ''' + ) + ) + + def test_fn_div_blank_lines(self): + """Raw HTML with blank lines in a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: +
+ + **x** + +
+ + after + ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '
    \n' + '\n' + '**x**\n' + '\n' + '
    \n' + '

    after ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_fn_text_then_div(self): + """Raw HTML after the text of a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: Note text. + +
+ **x** +
+ + after + ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '

    Note text.

    \n' + '
    \n' + '**x**\n' + '
    \n' + '

    after ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_fn_lazy_div(self): + """ + Indented HTML on a lazy line of a footnote is part of the paragraph. Regression: renders as it + always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: Note text. +
x
+ ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '

    Note text.\n' + '

    x
     ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_fn_code_with_html(self): + """Indented code with HTML in a footnote. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: Note. + +
+ + code +
+ ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '

    Note.

    \n' + '
    <div>\n'
    +            '\n'
    +            'code\n'
    +            '</div>\n'
    +            '
    \n' + '

    ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_fn_mdhtml(self): + """A `markdown` element with blank lines in a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: +
+ + **x** + +
+ ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '
    \n' + '

    x

    \n' + '
    \n' + '

    ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes', 'md_in_html'] + ) + + def test_fn_unclosed(self): + """An unclosed raw block in a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: +
+ + **x** + ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '

    \n' + '\n' + '**x** ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_dl_div_blank_lines(self): + """Raw HTML with blank lines in a definition. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : para + +
+ + **x** + +
+ + after + ''' + ), + self.dedent( + ''' +
+
Term
+
+

para

+
+ + **x** + +
+

after

+
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_dl_div_after_definition(self): + """Raw HTML after the text of a definition. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : definition + +
+ **x** +
+ ''' + ), + self.dedent( + ''' +
+
Term
+
+

definition

+
+ **x** +
+
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_dl_lazy_div(self): + """ + Indented HTML on a lazy line of a definition is part of the paragraph. Regression: renders as it + always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : definition +
x
+ ''' + ), + self.dedent( + ''' +
+
Term
+
definition +
x
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_dl_code_with_html(self): + """Indented code with HTML in a definition. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : definition + +
+ + code +
+ ''' + ), + self.dedent( + ''' +
+
Term
+
definition
<div>
+
+                code
+                </div>
+                
+
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_dl_mdhtml(self): + """A `markdown` element with blank lines in a definition. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : para + +
+ + **x** + +
+ ''' + ), + self.dedent( + ''' +
+
Term
+
+

para

+
+

x

+
+
+
+ ''' + ), + extensions=['def_list', 'md_in_html'] + ) + + def test_dl_unclosed(self): + """An unclosed raw block in a definition. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : para + +
+ + **x** + ''' + ), + self.dedent( + ''' +
+
Term
+
+

para

+
+ + **x** +
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_adm_in_footnote(self): + """Raw HTML in an admonition in a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Text[^1] + + [^1]: + !!! note +
+ + **x** + +
+ ''' + ), + '

Text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '
    \n' + '

    Note

    \n' + '
    \n' + '\n' + '**x**\n' + '\n' + '
    \n' + '
    \n' + '

    ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes', 'admonition'] + ) + + def test_list_in_admonition(self): + """Raw HTML with blank lines in a list in an admonition. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + - item + +
+ + **x** + +
+ + after + ''' + ), + self.dedent( + ''' +
+

Note

+
    +
  • +

    item

    +
    + + **x** + +
    +

    after

    +
  • +
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_admonition_in_list_in_definition(self): + """Three levels of containers. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : - item + + !!! note +
+ + **x** + +
+ ''' + ), + self.dedent( + ''' +
+
Term
+
+
    +
  • +

    item

    +
    +

    Note

    +
    + + **x** + +
    +
    +
  • +
+
+
+ ''' + ), + extensions=['def_list', 'admonition'] + ) + + def test_admonition_in_blockquote(self): + """ + An admonition in a blockquote is a container, so its indented HTML is raw. Intended change: a raw + block in a container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + > !!! note + >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
+
+

Note

+
+ **x** +
+
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_blockquote_in_admonition(self): + """A blockquote is a container too, so HTML in one is raw. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
+

Note

+
+
+ **x** +
+
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_blockquote_div(self): + """A block of HTML in a blockquote is raw. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
+
+ **x** +
+
+ ''' + ) + ) + + def test_blockquote_div_blank_lines(self): + """Blank lines in the raw block do not end it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > + > **x** + > + >
+ ''' + ), + self.dedent( + ''' +
+
+ + **x** + +
+
+ ''' + ) + ) + + def test_blockquote_div_after_paragraph(self): + """Raw block after a paragraph in a blockquote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > para + > + >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
+

para

+
+ **x** +
+
+ ''' + ) + ) + + def test_blockquote_div_then_paragraph(self): + """A paragraph follows the raw block. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > x + >
+ > + > after + ''' + ), + self.dedent( + ''' +
+
+ x +
+

after

+
+ ''' + ) + ) + + def test_blockquote_text_after_close(self): + """ + Text after the end of the raw block starts its own paragraph. Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + >
x
+ > more + ''' + ), + self.dedent( + ''' +
+
x
+

more

+
+ ''' + ) + ) + + def test_blockquote_two_blocks(self): + """Two raw blocks in a blockquote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
a
+ > + >
b
+ ''' + ), + self.dedent( + ''' +
+
a
+
b
+
+ ''' + ) + ) + + def test_blockquote_chained_blocks(self): + """Raw blocks that start where the last one ended. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
a
b
tail + ''' + ), + self.dedent( + ''' +
+
a
+
b
+

tail

+
+ ''' + ) + ) + + def test_blockquote_lazy_line_in_html(self): + """A lazy line inside the raw block belongs to it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ lazy + >
+ ''' + ), + self.dedent( + ''' +
+
+ lazy +
+
+ ''' + ) + ) + + def test_blockquote_lazy_line_after_html(self): + """ + A lazy line after the raw block is a paragraph in the blockquote. Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + >
x
+ lazy + ''' + ), + self.dedent( + ''' +
+
x
+

lazy

+
+ ''' + ) + ) + + def test_blockquote_unclosed_html(self): + """ + A raw block that is never closed ends with the blockquote. Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > x + + after + ''' + ), + self.dedent( + ''' +
+
+ x +
+

after

+ ''' + ) + ) + + def test_blockquote_nested(self): + """Raw block in a nested blockquote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > >
+ > > **x** + > >
+ ''' + ), + self.dedent( + ''' +
+
+
+ **x** +
+
+
+ ''' + ) + ) + + def test_blockquote_nested_after_text(self): + """Raw block in a blockquote nested after text. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > text + > >
+ > > x + > >
+ ''' + ), + self.dedent( + ''' +
+

text

+
+
+ x +
+
+
+ ''' + ) + ) + + def test_blockquote_in_list(self): + """Raw block in a blockquote in a list item. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + - item + + >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
    +
  • +

    item

    +
    +
    + **x** +
    +
    +
  • +
+ ''' + ) + ) + + def test_blockquote_on_list_marker(self): + """Raw block in a blockquote on the line of a list marker. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + - >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
    +
  • +
    +
    + **x** +
    +
    +
  • +
+ ''' + ) + ) + + def test_blockquote_comment(self): + """A comment in a blockquote is raw. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > + > text + ''' + ), + self.dedent( + ''' +
+ +

text

+
+ ''' + ) + ) + + def test_blockquote_pre_blank_line(self): + """A blank line in a raw block does not split it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+                > a
+                >
+                > b
+                > 
+ ''' + ), + self.dedent( + ''' +
+
+                a
+
+                b
+                
+
+ ''' + ) + ) + + def test_blockquote_script(self): + """The content of a raw block is not escaped. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > + ''' + ), + self.dedent( + ''' +
+ +
+ ''' + ) + ) + + def test_blockquote_table(self): + """A table in a blockquote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > + > + >
x
+ ''' + ), + self.dedent( + ''' +
+ + +
x
+
+ ''' + ) + ) + + def test_blockquote_md_in_html(self): + """A `markdown` attribute works in a blockquote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > **x** + >
+ ''' + ), + self.dedent( + ''' +
+
+

x

+
+
+ ''' + ), + extensions=['md_in_html'] + ) + + def test_blockquote_md_in_html_block(self): + """A `markdown` attribute with blank lines. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > + > **x** + > + >
+ ''' + ), + self.dedent( + ''' +
+
+

x

+
+
+ ''' + ), + extensions=['md_in_html'] + ) + + def test_blockquote_in_footnote(self): + """Raw block in a blockquote in a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + text[^1] + + [^1]: + >
+ > x + >
+ ''' + ), + '

text1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '
    \n' + '
    \n' + 'x\n' + '
    \n' + '
    \n' + '

    ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_blockquote_paragraph_continuation(self): + """A raw block can not interrupt a paragraph in a blockquote. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > a + >
x
+ ''' + ), + self.dedent( + ''' +
+

a +

x

+
+ ''' + ) + ) + + def test_blockquote_inline_tag(self): + """An inline tag in a blockquote is not a raw block. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > x text + ''' + ), + self.dedent( + ''' +
+

x text

+
+ ''' + ) + ) + + def test_blockquote_code_block(self): + """An indented code block in a blockquote is code. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + >
+ > x + >
+ ''' + ), + self.dedent( + ''' +
+
<div>
+                x
+                </div>
+                
+
+ ''' + ) + ) + + def test_blockquote_fenced_code(self): + """HTML in a fenced code block in a blockquote is code. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > ``` + >
+ > ``` + ''' + ), + self.dedent( + ''' +
+

<div>

+
+ ''' + ), + extensions=['fenced_code'] + ) + + def test_blockquote_list_marker_html(self): + """ + What follows a list marker in a blockquote is parsed as it always has been. Regression: renders as + it always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + > -
x
+ ''' + ), + self.dedent( + ''' +
+
    +
  • x
  • +
+
+ ''' + ) + ) + + def test_blockquote_invalid_declaration(self): + """ + Something the extractor does not treat as a raw block is left alone. Regression: renders as it + always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + > + > text + ''' + ), + self.dedent( + ''' +
+

<!invalid> + text

+
+ ''' + ) + ) + + def test_comment_blank_line_admonition(self): + """A comment with a blank line in it is one raw block. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + text + ''' + ), + self.dedent( + ''' +
+

Note

+ +

text

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_blank_line_list(self): + """A comment with a blank line in a list item. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + - a + + + text + ''' + ), + self.dedent( + ''' +
    +
  • +

    a

    + +

    text

    +
  • +
+ ''' + ) + ) + + def test_comment_blank_line_definition(self): + """A comment with a blank line in a definition. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + Term + : a + + + text + ''' + ), + self.dedent( + ''' +
+
Term
+
+

a

+ +

text

+
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_comment_blank_line_blockquote(self): + """A comment with a blank line in a blockquote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + > + > text + ''' + ), + self.dedent( + ''' +
+ +

text

+
+ ''' + ) + ) + + def test_comment_blank_line_blockquote_in_list(self): + """ + A comment with a blank line in a blockquote in a list item. Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + - a + + > + > text + ''' + ), + self.dedent( + ''' +
    +
  • +

    a

    +
    + +

    text

    +
    +
  • +
+ ''' + ) + ) + + def test_comment_blank_line_footnote(self): + """A comment with a blank line in a footnote. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + x[^1] + + [^1]: + + text + ''' + ), + '

x1

\n' + '
\n' + '
\n' + '
    \n' + '
  1. \n' + '\n' + '

    text ↩

    \n' + '
  2. \n' + '
\n' + '
', + extensions=['footnotes'] + ) + + def test_comment_many_blank_lines(self): + """A comment with several blank lines in it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + text + ''' + ), + self.dedent( + ''' +
+

Note

+ +

text

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_blank_line_then_div(self): + """A raw block follows a comment with a blank line. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + +
x
+ ''' + ), + self.dedent( + ''' +
+

Note

+ +
x
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_blank_line_two_comments(self): + """Two comments with blank lines in them. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + + + ''' + ), + self.dedent( + ''' +
+

Note

+ + +
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_blank_line_after_paragraph(self): + """A comment with a blank line after a paragraph. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + para + + + ''' + ), + self.dedent( + ''' +
+

Note

+

para

+ +
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_blank_line_div_after_close(self): + """A raw block starts on the line where the comment ends. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note +
x
+ ''' + ), + self.dedent( + ''' +
+

Note

+ +
x
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_blank_line_md_in_html(self): + """ + A `markdown` attribute after a comment with a blank line. Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + +
+ **x** +
+ ''' + ), + self.dedent( + ''' +
+

Note

+ +
+

x

+
+
+ ''' + ), + extensions=['admonition', 'md_in_html'] + ) + + def test_processing_instruction_blank_line(self): + """A processing instruction with a blank line in it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + ''' + ), + self.dedent( + ''' +
+

Note

+ +
+ ''' + ), + extensions=['admonition'] + ) + + def test_processing_instruction_blank_line_blockquote(self): + """ + A processing instruction with a blank line in a blockquote. Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + > c + > + > ?> + ''' + ), + self.dedent( + ''' +
+ +
+ ''' + ) + ) + + def test_cdata_blank_line(self): + """A `CDATA` section with a blank line in it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + ''' + ), + self.dedent( + ''' +
+

Note

+ +
+ ''' + ), + extensions=['admonition'] + ) + + def test_declaration_blank_line(self): + """A declaration with a blank line in it. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + ''' + ), + self.dedent( + ''' +
+

Note

+ +
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_unclosed_admonition(self): + """A comment which is never closed is text. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + + +

<!-- b

+

text

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_comment_in_text_line(self): + """A comment that does not start a block is text. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + text more + ''' + ), + self.dedent( + ''' +
+

Note

+

text <!-- + c

+

--> more

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_processing_instruction_unclosed(self): + """A processing instruction which is never closed is text. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + +

Note

+

<?php + c

+

text

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_cdata_unclosed(self): + """A `CDATA` section which is never closed is text. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + +

Note

+

<![CDATA[ + c

+

text

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_cdata_spaced_close(self): + """A `CDATA` section is closed by `]]>` only. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + + after + ''' + ), + self.dedent( + ''' +
+

Note

+

<![CDATA[ + c

+

] ]> + after

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_declaration_unclosed(self): + """A declaration which is never closed is text. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note + +

Note

+

<!DOCTYPE + c

+

text

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_list_lazy_continuation_div(self): + """Indented div right after a list item line (no blank line). Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + - item +
x
+ ''' + ), + self.dedent( + ''' + + ''' + ) + ) + + def test_code_block_in_list(self): + """ + Code block (8 spaces) inside a list item containing HTML with blank lines. Regression: renders as it + always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + - item + +
+ + code +
+ ''' + ), + self.dedent( + ''' + + ''' + ) + ) + + def test_code_block_in_admonition(self): + """Code block (8 spaces) inside an admonition containing HTML. Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" +
+ code +
+ ''' + ), + self.dedent( + ''' +
+

Title

+
<div>
+                code
+                </div>
+                
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_trailing_text_after_close(self): + """ + Text on the same line after the closing tag. Text after a block starts its own paragraph (as at the + root). Intended change: a raw block in a container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" +
x
trailing + + after + ''' + ), + self.dedent( + ''' +
+

Title

+
x
+

trailing

+

after

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_trailing_text_multiline_block(self): + """ + Multi-line block followed by text on the closing line, then a lazy continuation line. Intended + change: a raw block in a container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" +
+ **x** +
trailing + more + + after + ''' + ), + self.dedent( + ''' +
+

Title

+
+ **x** +
+

trailing + more

+

after

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_trailing_text_chained_blocks(self): + """ + A second block starts on the closing line of the first, followed by text. Intended change: a raw + block in a container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" +
a
b
trailing + + after + ''' + ), + self.dedent( + ''' +
+

Title

+
a
+
b
+

trailing

+

after

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_trailing_text_after_comment(self): + """ + Text after an indented comment on the same line (empty-tag path). Intended change: a raw block in a + container. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" + trailing + + after + ''' + ), + self.dedent( + ''' +
+

Title

+ +

trailing

+

after

+
+ ''' + ), + extensions=['admonition'] + ) + + def test_trailing_text_in_list(self): + """Text after the closing tag inside a list item. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + - item + +
x
trailing + + after + ''' + ), + self.dedent( + ''' + + ''' + ) + ) + + def test_trailing_text_in_code_block(self): + """ + Indented CODE with HTML followed by text on the same line must stay one code line. Regression: + renders as it always has. + """ + + self.assertMarkdownRenders( + self.dedent( + R''' + text + +
x
trailing + more code + ''' + ), + self.dedent( + ''' +

text

+
<div>x</div> trailing
+                more code
+                
+ ''' + ) + ) + + def test_trailing_text_in_code_block_in_admonition(self): + """Same inside an admonition (8 spaces). Regression: renders as it always has.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Title" +

x

trailing + more code + ''' + ), + self.dedent( + ''' +
+

Title

+
<p>x</p> trailing
+                more code
+                
+
+ ''' + ), + extensions=['admonition'] + ) + + def test_trailing_text_nested_admonition(self): + """Two container levels. Intended change: a raw block in a container.""" + + self.assertMarkdownRenders( + self.dedent( + R''' + !!! note "Outer" + !!! note "Inner" +
x
trailing + + after + ''' + ), + self.dedent( + ''' +
+

Outer

+
+

Inner

+
x
+

trailing

+

after

+
+
+ ''' + ), + extensions=['admonition'] + ) + + +class SectionProcessor(BlockProcessor): + """ + Parse the indented blocks after `::: section` into an element of its own, then move what was parsed into a + `section`. The element parsed into is never part of the document and is not handed anything more once the call + to the parser returns. + """ + + def test(self, parent, block): + return block.split('\n', 1)[0] == '::: section' + + def run(self, parent, blocks): + chunks = [blocks.pop(0).split('\n', 1)[1]] + while blocks and blocks[0].startswith(' '): + chunks.append(blocks.pop(0)) + temp = etree.Element('div') + self.parser.parseChunk(temp, '\n'.join(line[4:] for line in '\n\n'.join(chunks).split('\n'))) + section = etree.SubElement(parent, 'section') + for el in list(temp): + section.append(el) + + +class SectionExtension(Extension): + def extendMarkdown(self, md): + md.parser.blockprocessors.register(SectionProcessor(md.parser), 'section', 105) + + +class TestTemporaryParent(TestCase): + """ + A raw block held open in an element an extension parses into on its own must be written out before the call to + the parser returns. Holding it until the end of the document puts it in an element nothing refers to by then. + """ + + def test_unclosed_raw_block(self): + self.assertMarkdownRenders( + self.dedent( + ''' + ::: section +
+ unclosed + + kept + + after + ''' + ), + self.dedent( + ''' +
+
+ unclosed + + kept +
+

after

+ ''' + ), + extensions=[SectionExtension()] + ) + + def test_unterminated_comment(self): + self.assertMarkdownRenders( + self.dedent( + ''' + ::: section + after + + para + ''' + ), + self.dedent( + ''' +
    +
  • +

    item

    + +

    after

    +

    para

    +
  • +
+ ''' + ) + ) + + def test_processing_instruction_ended_blocks_later(self): + self.assertMarkdownRenders( + self.dedent( + ''' + - item + + + + para + ''' + ), + self.dedent( + ''' +
    +
  • +

    item

    + +

    para

    +
  • +
+ ''' + ) + ) + + def test_many_blocks_in_unterminated_comment(self): + """ + Each block held for an unterminated comment should only be searched for a terminator itself, as searching + everything held so far for each block takes quadratic time. + """ + count = 5000 + self.assertMarkdownRenders( + '- item\n\n + + - d + ''' + ), + self.dedent( + ''' +
    +
  • + +
  • +
  • +

    d

    +
  • +
+ ''' + ) + ) + + def test_definition(self): + """A raw block right after the colon of a definition.""" + + self.assertMarkdownRenders( + self.dedent( + ''' + Term + :
*x*
+ ''' + ), + self.dedent( + ''' +
+
Term
+
*x*
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_loose_definition(self): + self.assertMarkdownRenders( + self.dedent( + ''' + Term + + :
*x*
+ ''' + ), + self.dedent( + ''' +
+
Term
+
+
*x*
+
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_definition_indent(self): + """The rest of a definition is already detabbed, so the indent is removed only once.""" + + self.assertMarkdownRenders( + self.dedent( + ''' + Term + :
+                    code
+                      more
+                    
+ ''' + ), + self.dedent( + ''' +
+
Term
+
+                code
+                  more
+                
+
+ ''' + ), + extensions=['def_list'] + ) + + def test_md_in_html_tight_list_item(self): + """Parsed as it would be at the top of the document: a tight item's text has no `p`, but this does.""" + + self.assertMarkdownRenders( + self.dedent( + ''' + -
*x*
+ - two + ''' + ), + self.dedent( + ''' +
    +
  • +
    +

    x

    +
    +
  • +
  • two
  • +
+ ''' + ), + extensions=['md_in_html'] + ) + + def test_md_in_html_indented_content(self): + self.assertMarkdownRenders( + self.dedent( + ''' + -
+ *x* + + para +
+ + - two + ''' + ), + self.dedent( + ''' +
    +
  • +
    +

    x

    +

    para

    +
    +
  • +
  • +

    two

    +
  • +
+ ''' + ), + extensions=['md_in_html'] + ) + + def test_md_in_html_definition(self): + self.assertMarkdownRenders( + self.dedent( + ''' + Term + :
*x*
+ ''' + ), + self.dedent( + ''' +
+
Term
+
+
+

x

+
+
+
+ ''' + ), + extensions=['md_in_html', 'def_list'] + )