diff --git a/docs/changelog.md b/docs/changelog.md index 94b4a2f5..ed015393 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -23,6 +23,8 @@ See the [Contributing Guide](contributing.md) for details. and closing backticks did not match (#1668). * Escape the language name in the `codehilite` extension's non-Pygments output to maintain consistency with Pygments output. +* Fix a list item being merged into the previous item when it directly follows an + indented paragraph or nested list of that item without a blank line (#845). ## [3.11.2] - 2026-10-19 diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index cbad917b..75486c77 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -180,6 +180,7 @@ def test(self, parent: etree.Element, block: str) -> bool: def run(self, parent: etree.Element, blocks: list[str]) -> None: block = blocks.pop(0) + block, rest = self.split_list_items(parent, block) level, sibling = self.get_level(parent, block) block = self.looseDetab(block, level) @@ -213,6 +214,29 @@ def run(self, parent: etree.Element, blocks: list[str]) -> None: else: self.create_item(sibling, block) self.parser.state.reset() + if rest: + # No blank line comes before these items, so they continue the list as + # tight items (see `OListProcessor`). + self.parser.state.set('listcontinue') + self.parser.parseBlocks(parent, [rest]) + self.parser.state.reset() + + def split_list_items(self, parent: etree.Element, block: str) -> tuple[str, str]: + """ Split off the unindented list items which end an indented block. + + Return the part of `block` before the first unindented line which starts another + item of the list in `parent`, and the rest, which is empty if there is no such line. + """ + if parent.tag in self.ITEM_TYPES: + return block, '' + lst = parent[-1] + lines = block.split('\n') + for i, line in enumerate(lines[1:], start=1): + for processor in self.parser.blockprocessors: + if (isinstance(processor, OListProcessor) and lst.tag in processor.SIBLING_TAGS and + processor.test(parent, line)): + return '\n'.join(lines[:i]), '\n'.join(lines[i:]) + return block, '' def create_item(self, parent: etree.Element, block: str) -> None: """ Create a new `li` and parse the block with it as the parent. """ @@ -361,7 +385,12 @@ def run(self, parent: etree.Element, blocks: list[str]) -> None: items = self.get_items(blocks.pop(0)) sibling = self.lastChild(parent) - if sibling is not None and sibling.tag in self.SIBLING_TAGS: + if (sibling is not None and sibling.tag in self.SIBLING_TAGS and + self.parser.state.isstate('listcontinue')): + # These items directly follow an indented block of the previous item + # without a blank line (see `ListIndentProcessor`), so they are tight. + lst = sibling + elif sibling is not None and sibling.tag in self.SIBLING_TAGS: # Previous block was a list item, so set that as parent lst = sibling # make sure previous item is in a `p` - if the item has text, diff --git a/tests/test_syntax/blocks/test_ul.py b/tests/test_syntax/blocks/test_ul.py index f90d43a2..1088968b 100644 --- a/tests/test_syntax/blocks/test_ul.py +++ b/tests/test_syntax/blocks/test_ul.py @@ -135,3 +135,182 @@ def test_header_and_paragraph_no_blank_line_tight_list_no_indent(self): """ ) ) + + # An unindented list item ends the indented block before it, even without a blank + # line. As no blank line comes before the new item, it is not a loose item (#845). + def test_item_after_indented_paragraph_no_blank_line(self): + self.assertMarkdownRenders( + self.dedent( + """ + - item one + + second paragraph of item 1 + - item two + - item three + """ + ), + self.dedent( + """ +
item one
+second paragraph of item 1
+item one
+second paragraph of item 1
+First item:
+Additional paragraph in first item
+item one
+second paragraph of item 1 + line two of second paragraph of item 1
+++ """ + ) + ) + + # As with a list at the top level, a horizontal rule ends the list. + def test_hr_after_indented_paragraph(self): + self.assertMarkdownRenders( + self.dedent( + """ + - item one + + second paragraph of item 1 + * * * + """ + ), + self.dedent( + """ ++
+- +
+item one
+second paragraph of item 1
+- item two
+
item one
+second paragraph of item 1
+item one
+second paragraph of item 1 + - not an item
+