diff --git a/docs/changelog.md b/docs/changelog.md index f3335f1e5..3d7734939 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -41,6 +41,10 @@ See the [Contributing Guide](contributing.md) for details. provided, rather than rendering them as `key="key"`, so the same source renders consistently under both the `html` and `xhtml` output formats (#1501). +* Treat a block-level raw HTML tag indented by more than three spaces as a raw + HTML block when it is nested in a containing block (a list item, an + admonition, etc.), instead of splitting it into paragraphs at blank lines. + Within an indented code block such HTML is still rendered as code (#1096). ## [3.11.2] - 2026-10-19 diff --git a/markdown/blockparser.py b/markdown/blockparser.py index 549c9ecd5..ce54b4221 100644 --- a/markdown/blockparser.py +++ b/markdown/blockparser.py @@ -96,6 +96,7 @@ def __init__(self, md: Markdown): self.blockprocessors: util.Registry[BlockProcessor] = util.Registry() self.state = State() self.md = md + self._depth = 0 def parseDocument(self, lines: Iterable[str]) -> etree.ElementTree: """ Parse a Markdown document into an `ElementTree`. @@ -113,6 +114,7 @@ def parseDocument(self, lines: Iterable[str]) -> etree.ElementTree: An element tree. """ # Create an `ElementTree` from the lines + self._depth = 0 self.root = etree.Element(self.md.doc_tag) self.parseChunk(self.root, '\n'.join(lines)) return etree.ElementTree(self.root) @@ -152,9 +154,17 @@ def parseBlocks(self, parent: etree.Element, blocks: list[str]) -> None: blocks: The blocks of text to parse. """ - while blocks: + self._depth += 1 + 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._depth -= 1 + if not self._depth: + # 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 d3574045f..a503b3338 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,154 @@ 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 + + +class HeldBlocks(list): + """ Blocks which are held back until it is known whether they are raw HTML, and the parser state they are in. """ + + def __init__(self, blocks: list[str], state: list): + super().__init__(blocks) + self.state = list(state) + + +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'] + )