From cf54e90ae2a77eca6a9fa789d106a5306c14d8ff Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 9 Apr 2026 16:43:09 +0100 Subject: [PATCH] Fix Markdown blockquote lazy continuation parsing - Headings (`# foo`), bullet lists, ordered lists, and code fences now break out of a blockquote instead of being lazily continued. - Unquoted blank lines end the blockquote; only `>`-prefixed blank lines continue it. - Heading lookahead uses `!(AtxStart @Spacechar)` so `#no-space` text (not a valid heading) is correctly kept inside the blockquote. - Code fence lookahead is gated behind `&{ github? }` so it only applies when the GitHub extension is enabled, matching `CodeFence`. Ref: https://github.com/ruby/rdoc/pull/1627 --- lib/rdoc/markdown.kpeg | 4 +- lib/rdoc/markdown.rb | 186 +++++++++++++++++++++++++++----- test/rdoc/rdoc_markdown_test.rb | 85 ++++++++++++++- 3 files changed, 246 insertions(+), 29 deletions(-) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index 8d24cc097f..0faef4f7cc 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -617,8 +617,8 @@ BlockQuote = BlockQuoteRaw:a BlockQuoteRaw = @StartList:a (( ">" " "? Line:l { a << l } ) - ( !">" !@BlankLine Line:c { a << c } )* - ( @BlankLine:n { a << n } )* + ( !">" !@BlankLine !(AtxStart @Spacechar) !Bullet !Enumerator !( &{ github? } Ticks3 ) Line:c { a << c } )* + ( ">" @BlankLine:n { a << n } )* )+ { inner_parse a.join } diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index 45954fd1cc..d003857bb0 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -1656,7 +1656,7 @@ def _BlockQuote return _tmp end - # BlockQuoteRaw = @StartList:a (">" " "? Line:l { a << l } (!">" !@BlankLine Line:c { a << c })* (@BlankLine:n { a << n })*)+ { inner_parse a.join } + # BlockQuoteRaw = @StartList:a (">" " "? Line:l { a << l } (!">" !@BlankLine !(AtxStart @Spacechar) !Bullet !Enumerator !(&{ github? } Ticks3) Line:c { a << c })* (">" @BlankLine:n { a << n })*)+ { inner_parse a.join } def _BlockQuoteRaw _save = self.pos @@ -1718,6 +1718,68 @@ def _BlockQuoteRaw self.pos = _save5 break end + _save8 = self.pos + + _save9 = self.pos + while true # sequence + _tmp = apply(:_AtxStart) + unless _tmp + self.pos = _save9 + break + end + _tmp = _Spacechar() + unless _tmp + self.pos = _save9 + end + break + end # end sequence + + _tmp = _tmp ? nil : true + self.pos = _save8 + unless _tmp + self.pos = _save5 + break + end + _save10 = self.pos + _tmp = apply(:_Bullet) + _tmp = _tmp ? nil : true + self.pos = _save10 + unless _tmp + self.pos = _save5 + break + end + _save11 = self.pos + _tmp = apply(:_Enumerator) + _tmp = _tmp ? nil : true + self.pos = _save11 + unless _tmp + self.pos = _save5 + break + end + _save12 = self.pos + + _save13 = self.pos + while true # sequence + _save14 = self.pos + _tmp = begin; github? ; end + self.pos = _save14 + unless _tmp + self.pos = _save13 + break + end + _tmp = apply(:_Ticks3) + unless _tmp + self.pos = _save13 + end + break + end # end sequence + + _tmp = _tmp ? nil : true + self.pos = _save12 + unless _tmp + self.pos = _save5 + break + end _tmp = apply(:_Line) c = @result unless _tmp @@ -1741,18 +1803,23 @@ def _BlockQuoteRaw end while true - _save9 = self.pos + _save16 = self.pos while true # sequence + _tmp = match_string(">") + unless _tmp + self.pos = _save16 + break + end _tmp = _BlankLine() n = @result unless _tmp - self.pos = _save9 + self.pos = _save16 break end @result = begin; a << n ; end _tmp = true unless _tmp - self.pos = _save9 + self.pos = _save16 end break end # end sequence @@ -1769,65 +1836,127 @@ def _BlockQuoteRaw if _tmp while true - _save10 = self.pos + _save17 = self.pos while true # sequence _tmp = match_string(">") unless _tmp - self.pos = _save10 + self.pos = _save17 break end - _save11 = self.pos + _save18 = self.pos _tmp = match_string(" ") unless _tmp _tmp = true - self.pos = _save11 + self.pos = _save18 end unless _tmp - self.pos = _save10 + self.pos = _save17 break end _tmp = apply(:_Line) l = @result unless _tmp - self.pos = _save10 + self.pos = _save17 break end @result = begin; a << l ; end _tmp = true unless _tmp - self.pos = _save10 + self.pos = _save17 break end while true - _save13 = self.pos + _save20 = self.pos while true # sequence - _save14 = self.pos + _save21 = self.pos _tmp = match_string(">") _tmp = _tmp ? nil : true - self.pos = _save14 + self.pos = _save21 unless _tmp - self.pos = _save13 + self.pos = _save20 break end - _save15 = self.pos + _save22 = self.pos _tmp = _BlankLine() _tmp = _tmp ? nil : true - self.pos = _save15 + self.pos = _save22 + unless _tmp + self.pos = _save20 + break + end + _save23 = self.pos + + _save24 = self.pos + while true # sequence + _tmp = apply(:_AtxStart) + unless _tmp + self.pos = _save24 + break + end + _tmp = _Spacechar() + unless _tmp + self.pos = _save24 + end + break + end # end sequence + + _tmp = _tmp ? nil : true + self.pos = _save23 + unless _tmp + self.pos = _save20 + break + end + _save25 = self.pos + _tmp = apply(:_Bullet) + _tmp = _tmp ? nil : true + self.pos = _save25 unless _tmp - self.pos = _save13 + self.pos = _save20 + break + end + _save26 = self.pos + _tmp = apply(:_Enumerator) + _tmp = _tmp ? nil : true + self.pos = _save26 + unless _tmp + self.pos = _save20 + break + end + _save27 = self.pos + + _save28 = self.pos + while true # sequence + _save29 = self.pos + _tmp = begin; github? ; end + self.pos = _save29 + unless _tmp + self.pos = _save28 + break + end + _tmp = apply(:_Ticks3) + unless _tmp + self.pos = _save28 + end + break + end # end sequence + + _tmp = _tmp ? nil : true + self.pos = _save27 + unless _tmp + self.pos = _save20 break end _tmp = apply(:_Line) c = @result unless _tmp - self.pos = _save13 + self.pos = _save20 break end @result = begin; a << c ; end _tmp = true unless _tmp - self.pos = _save13 + self.pos = _save20 end break end # end sequence @@ -1836,23 +1965,28 @@ def _BlockQuoteRaw end _tmp = true unless _tmp - self.pos = _save10 + self.pos = _save17 break end while true - _save17 = self.pos + _save31 = self.pos while true # sequence + _tmp = match_string(">") + unless _tmp + self.pos = _save31 + break + end _tmp = _BlankLine() n = @result unless _tmp - self.pos = _save17 + self.pos = _save31 break end @result = begin; a << n ; end _tmp = true unless _tmp - self.pos = _save17 + self.pos = _save31 end break end # end sequence @@ -1861,7 +1995,7 @@ def _BlockQuoteRaw end _tmp = true unless _tmp - self.pos = _save10 + self.pos = _save17 end break end # end sequence @@ -16467,7 +16601,7 @@ def _DefinitionListDefinition Rules[:_SetextHeading2] = rule_info("SetextHeading2", "&(@RawLine SetextBottom2) @StartList:a (!@Endline Inline:b { a << b })+ @Sp @Newline SetextBottom2 { RDoc::Markup::Heading.new(2, a.join) }") Rules[:_Heading] = rule_info("Heading", "(SetextHeading | AtxHeading)") Rules[:_BlockQuote] = rule_info("BlockQuote", "BlockQuoteRaw:a { RDoc::Markup::BlockQuote.new(*a) }") - Rules[:_BlockQuoteRaw] = rule_info("BlockQuoteRaw", "@StartList:a (\">\" \" \"? Line:l { a << l } (!\">\" !@BlankLine Line:c { a << c })* (@BlankLine:n { a << n })*)+ { inner_parse a.join }") + Rules[:_BlockQuoteRaw] = rule_info("BlockQuoteRaw", "@StartList:a (\">\" \" \"? Line:l { a << l } (!\">\" !@BlankLine !(AtxStart @Spacechar) !Bullet !Enumerator !(&{ github? } Ticks3) Line:c { a << c })* (\">\" @BlankLine:n { a << n })*)+ { inner_parse a.join }") Rules[:_NonblankIndentedLine] = rule_info("NonblankIndentedLine", "!@BlankLine IndentedLine") Rules[:_VerbatimChunk] = rule_info("VerbatimChunk", "@BlankLine*:a NonblankIndentedLine+:b { a.concat b }") Rules[:_Verbatim] = rule_info("Verbatim", "VerbatimChunk+:a { RDoc::Markup::Verbatim.new(*a.flatten) }") diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb index 5ffed64f78..5861720860 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -120,12 +120,95 @@ def test_parse_block_quote_separate expected = doc( block( - para("this is\na block quote"), + para("this is\na block quote")), + block( para("that continues"))) assert_equal expected, doc end + def test_parse_block_quote_no_lazy_continuation_for_heading + doc = parse <<~BLOCK_QUOTE + > foo + # bar + BLOCK_QUOTE + + expected = + doc( + block( + para("foo")), + head(1, "bar")) + + assert_equal expected, doc + end + + def test_parse_block_quote_no_lazy_continuation_for_list + doc = parse <<~BLOCK_QUOTE + > foo + - bar + BLOCK_QUOTE + + expected = + doc( + block( + para("foo")), + list(:BULLET, + item(nil, para("bar")))) + + assert_equal expected, doc + end + + def test_parse_block_quote_no_lazy_continuation_for_ordered_list + doc = parse <<~BLOCK_QUOTE + > foo + 1. bar + BLOCK_QUOTE + + expected = + doc( + block( + para("foo")), + list(:NUMBER, + item(nil, para("bar")))) + + assert_equal expected, doc + end + + def test_parse_block_quote_no_lazy_continuation_for_code_fence + doc = parse <<~BLOCK_QUOTE + > foo + ``` + code + ``` + BLOCK_QUOTE + + expected = + doc( + block( + para("foo")), + verb("code\n")) + + assert_equal expected, doc + end + + def test_parse_block_quote_lazy_continuation_for_code_fence_non_github + @parser.github = false + + doc = parse <<~BLOCK_QUOTE + > foo + ``` + code + ``` + BLOCK_QUOTE + + expected = + doc( + block( + para("foo\n\ncode\n"))) + + assert_equal expected, doc + end + def test_parse_char_entity doc = parse 'π &nn;'