From 91e21cb86e2f9888bd13cec8bb9b2d07b4744fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Fri, 4 Sep 2026 12:48:51 +0100 Subject: [PATCH] Fix a blank line with partial indent evicting a list item early Reported in #618: a blank line inside a list item, made up only of spaces that fall short of the item's own indent width, would end the item and push whatever came after out as a sibling paragraph, even though a fully empty blank line or one padded to the full indent width kept the item open just fine. The three cases only diverge when the item's opening line consisted solely of a link reference definition. Once the reference resolves, finalize() frees that now-empty paragraph, and parse_node_item_prefix was using container->first_child == NULL as a proxy for "the opening line was blank", since that's normally the only way an item ends up childless. A reference-only opening line hits the same childless state for an unrelated reason, so a later short blank line got read as "this item never had anything in it" and the item closed prematurely. A blank line with zero or full-width indent happened to avoid the branch in parse_node_item_prefix that checks first_child at all, which is why only the partial-indent case showed the bug. Added a CMARK_NODE__ITEM_HAD_CONTENT flag, set on the item when a reference-only paragraph belonging to it is freed, and checked alongside first_child in parse_node_item_prefix. Added a regression test to test/regression.txt using the exact shape from the issue. Ran the full local suite (api tests, CommonMark spec tests, smart punctuation tests, and the regression suite) before and after: all green after, and the new case fails without the fix, confirmed by temporarily reverting just the blocks.c change and rebuilding. --- src/blocks.c | 22 +++++++++++++++++----- src/node.h | 1 + test/regression.txt | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/blocks.c b/src/blocks.c index 8ff339ee4..164462090 100644 --- a/src/blocks.c +++ b/src/blocks.c @@ -289,7 +289,12 @@ static cmark_node *finalize(cmark_parser *parser, cmark_node *b) { { has_content = resolve_reference_link_definitions(parser); if (!has_content) { - // remove blank node (former reference def) + // remove blank node (former reference def), but if it was the + // opening line of a list item, remember that the item did have + // real content, since parent->first_child is about to go back + // to NULL along with this node. + if (parent && S_type(parent) == CMARK_NODE_ITEM) + parent->flags |= CMARK_NODE__ITEM_HAD_CONTENT; cmark_node_free(b); } else { b->len = node_content->size; @@ -805,10 +810,17 @@ static bool parse_node_item_prefix(cmark_parser *parser, cmark_chunk *input, container->as.list.padding, true); res = true; - } else if (parser->blank && container->first_child != NULL) { - // if container->first_child is NULL, then the opening line - // of the list item was blank after the list marker; in this - // case, we are done with the list item. + } else if (parser->blank && + (container->first_child != NULL || + (container->flags & CMARK_NODE__ITEM_HAD_CONTENT))) { + // if container->first_child is NULL and the item never had any + // other content either, then the opening line of the list item + // was blank after the list marker; in this case, we are done with + // the list item. CMARK_NODE__ITEM_HAD_CONTENT covers the case + // where the item's only line so far was a link reference + // definition: its paragraph gets freed once the reference + // resolves, leaving first_child NULL again even though the + // opening line was not blank. S_advance_offset(parser, input, parser->first_nonspace - parser->offset, false); res = true; diff --git a/src/node.h b/src/node.h index 3d9ddcf54..949160af7 100644 --- a/src/node.h +++ b/src/node.h @@ -51,6 +51,7 @@ enum cmark_node__internal_flags { CMARK_NODE__LAST_LINE_BLANK = (1 << 1), CMARK_NODE__LAST_LINE_CHECKED = (1 << 2), CMARK_NODE__LIST_LAST_LINE_BLANK = (1 << 3), + CMARK_NODE__ITEM_HAD_CONTENT = (1 << 4), }; struct cmark_node { diff --git a/test/regression.txt b/test/regression.txt index 1426617ab..20d6b4be2 100644 --- a/test/regression.txt +++ b/test/regression.txt @@ -349,3 +349,19 @@ break, not a paragraph.

foo

```````````````````````````````` + +Issue #618: a blank line inside a list item that has only trailing +spaces (fewer than the item's own indent) should not evict a later +indented line from the item, the same as a fully empty blank line or +one padded out to the item's indent width would. + +```````````````````````````````` example +- [r]:x + + + l +. + +````````````````````````````````