From 9701add906ef55d093fd3d25d8698dce3feebf88 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 25 Jun 2026 17:00:55 +0100 Subject: [PATCH] fix: keep empty list after blockquote as a sibling block A blockquote followed by a bare list marker line (for example `> foo\n-`) wrongly nested an empty list inside the blockquote. The blockquote regex reuses the paragraph list-interrupt clause ` {0,3}(?:[*+-]|1[.)])[ \t]+[^ \t\n]`, whose trailing `[ \t]+[^ \t\n]` requires content after the marker. A bare marker line therefore was not seen as an interruption and got lazily continued into the blockquote paragraph, then re-lexed into a nested empty list. Give the blockquote its own paragraph variant whose list-interrupt clause also matches a bare marker, so the list ends the blockquote and becomes a sibling block. The top level paragraph rule is unchanged, so an empty list still cannot interrupt a paragraph. --- src/rules.ts | 17 ++++++++++++++++- test/specs/new/empty_list_after_blockquote.html | 13 +++++++++++++ test/specs/new/empty_list_after_blockquote.md | 6 ++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test/specs/new/empty_list_after_blockquote.html create mode 100644 test/specs/new/empty_list_after_blockquote.md diff --git a/src/rules.ts b/src/rules.ts index cc2e62bccf..62e04ee512 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -175,8 +175,23 @@ const paragraph = edit(_paragraph) .replace('tag', _tag) // pars can be interrupted by type (6) html blocks .getRegex(); +// inside a blockquote a bare list marker starts a sibling list, so it must +// not be lazily continued as paragraph text (unlike a top level paragraph, +// where an empty list cannot interrupt) +const blockquoteParagraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)])(?:[ \\t]|\\n|$)') // a bare list marker also interrupts + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) - .replace('paragraph', paragraph) + .replace('paragraph', blockquoteParagraph) .getRegex(); /** diff --git a/test/specs/new/empty_list_after_blockquote.html b/test/specs/new/empty_list_after_blockquote.html new file mode 100644 index 0000000000..0ea2962633 --- /dev/null +++ b/test/specs/new/empty_list_after_blockquote.html @@ -0,0 +1,13 @@ +
+

foo

+
+ +
+

foo +bar

+
+
    +
  1. +
diff --git a/test/specs/new/empty_list_after_blockquote.md b/test/specs/new/empty_list_after_blockquote.md new file mode 100644 index 0000000000..33ee472888 --- /dev/null +++ b/test/specs/new/empty_list_after_blockquote.md @@ -0,0 +1,6 @@ +> foo +- + +> foo +> bar +1.