From 021d21edc10f9dccf8267dc09d38dff30f2f53f6 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Thu, 14 May 2026 16:58:30 +0200 Subject: [PATCH 1/2] Reset Markdown note state between parses Inline notes inside reference labels can run during the reference-gathering pass. Reset note state for each parse and reject inline notes before note ordering starts so reused parser instances do not accept stale footnote data. --- lib/rdoc/markdown.kpeg | 6 +++++- lib/rdoc/markdown.rb | 10 +++++++--- test/rdoc/rdoc_markdown_test.rb | 12 ++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index 0e15a604f4..847c02e01b 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -437,6 +437,8 @@ def parse markdown @references = {} @unlinked_references = {} + @footnotes = nil + @note_order = nil markdown += "\n\n" @@ -1234,7 +1236,9 @@ InlineNote = &{ notes? } @StartList:a ( !"]" Inline:l { a << l } )+ "]" - { ref = [:inline, @note_order.length] + { raise ParseError, 'invalid inline note' unless @note_order + + ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index c01cab0096..e10997ef75 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -822,6 +822,8 @@ def paragraph parts def parse markdown @references = {} @unlinked_references = {} + @footnotes = nil + @note_order = nil markdown += "\n\n" @@ -15476,7 +15478,7 @@ def _Note return _tmp end - # InlineNote = &{ notes? } "^[" @StartList:a (!"]" Inline:l { a << l })+ "]" { ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref } + # InlineNote = &{ notes? } "^[" @StartList:a (!"]" Inline:l { a << l })+ "]" { raise ParseError, 'invalid inline note' unless @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref } def _InlineNote _save = self.pos @@ -15567,7 +15569,9 @@ def _InlineNote self.pos = _save break end - @result = begin; ref = [:inline, @note_order.length] + @result = begin; raise ParseError, 'invalid inline note' unless @note_order + + ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref @@ -16841,7 +16845,7 @@ def _DefinitionListDefinition Rules[:_NoteReference] = rule_info("NoteReference", "&{ notes? } RawNoteReference:ref { note_for ref }") Rules[:_RawNoteReference] = rule_info("RawNoteReference", "\"[^\" < (!@Newline !\"]\" .)+ > \"]\" { text }") Rules[:_Note] = rule_info("Note", "&{ notes? } @NonindentSpace RawNoteReference:ref \":\" @Sp @StartList:a RawNoteBlock:i { a.concat i } (&Indent RawNoteBlock:i { a.concat i })* { @footnotes[ref] = paragraph a nil }") - Rules[:_InlineNote] = rule_info("InlineNote", "&{ notes? } \"^[\" @StartList:a (!\"]\" Inline:l { a << l })+ \"]\" { ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref }") + Rules[:_InlineNote] = rule_info("InlineNote", "&{ notes? } \"^[\" @StartList:a (!\"]\" Inline:l { a << l })+ \"]\" { raise ParseError, 'invalid inline note' unless @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref }") Rules[:_Notes] = rule_info("Notes", "(Note | SkipBlock)*") Rules[:_RawNoteBlock] = rule_info("RawNoteBlock", "@StartList:a (!@BlankLine !RawNoteReference OptionallyIndentedLine:l { a << l })+ < @BlankLine* > { a << text } { a }") Rules[:_CodeFence] = rule_info("CodeFence", "&{ github? } Ticks3 (@Sp StrChunk:format)? @Sp @Newline? < ((!\"`\" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }") diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb index 646f12a999..136b509b1d 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -1002,6 +1002,18 @@ def test_parse_note_inline assert_equal expected, doc end + def test_parse_note_inline_in_reference_label_after_reuse + @parser.notes = true + + parse "Some text. ^[With a footnote]" + + error = assert_raise RDoc::Markdown::ParseError do + parse "[foo ^[note]]: /url\n" + end + + assert_equal 'invalid inline note', error.message + end + def test_parse_note_no_notes @parser.notes = false From c30a01888bfbb6c5e84795fab21a1015790f68b1 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Thu, 14 May 2026 20:58:03 +0200 Subject: [PATCH 2/2] Ignore inline notes before note ordering Inline notes can be parsed while Markdown references are being collected, before footnote ordering has been initialized. Treat that early pass as a no-op so malformed reference labels do not crash the parser. --- lib/rdoc/markdown.kpeg | 12 +++++------- lib/rdoc/markdown.rb | 16 +++++++--------- test/rdoc/rdoc_markdown_test.rb | 10 ++-------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index 847c02e01b..7ca3eb9e59 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -437,8 +437,6 @@ def parse markdown @references = {} @unlinked_references = {} - @footnotes = nil - @note_order = nil markdown += "\n\n" @@ -1236,12 +1234,12 @@ InlineNote = &{ notes? } @StartList:a ( !"]" Inline:l { a << l } )+ "]" - { raise ParseError, 'invalid inline note' unless @note_order + { if @note_order + ref = [:inline, @note_order.length] + @footnotes[ref] = paragraph a - ref = [:inline, @note_order.length] - @footnotes[ref] = paragraph a - - note_for ref + note_for ref + end } Notes = ( Note | SkipBlock )* diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index e10997ef75..e9e2912df1 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -822,8 +822,6 @@ def paragraph parts def parse markdown @references = {} @unlinked_references = {} - @footnotes = nil - @note_order = nil markdown += "\n\n" @@ -15478,7 +15476,7 @@ def _Note return _tmp end - # InlineNote = &{ notes? } "^[" @StartList:a (!"]" Inline:l { a << l })+ "]" { raise ParseError, 'invalid inline note' unless @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref } + # InlineNote = &{ notes? } "^[" @StartList:a (!"]" Inline:l { a << l })+ "]" { if @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref end } def _InlineNote _save = self.pos @@ -15569,12 +15567,12 @@ def _InlineNote self.pos = _save break end - @result = begin; raise ParseError, 'invalid inline note' unless @note_order + @result = begin; if @note_order + ref = [:inline, @note_order.length] + @footnotes[ref] = paragraph a - ref = [:inline, @note_order.length] - @footnotes[ref] = paragraph a - - note_for ref + note_for ref + end ; end _tmp = true unless _tmp @@ -16845,7 +16843,7 @@ def _DefinitionListDefinition Rules[:_NoteReference] = rule_info("NoteReference", "&{ notes? } RawNoteReference:ref { note_for ref }") Rules[:_RawNoteReference] = rule_info("RawNoteReference", "\"[^\" < (!@Newline !\"]\" .)+ > \"]\" { text }") Rules[:_Note] = rule_info("Note", "&{ notes? } @NonindentSpace RawNoteReference:ref \":\" @Sp @StartList:a RawNoteBlock:i { a.concat i } (&Indent RawNoteBlock:i { a.concat i })* { @footnotes[ref] = paragraph a nil }") - Rules[:_InlineNote] = rule_info("InlineNote", "&{ notes? } \"^[\" @StartList:a (!\"]\" Inline:l { a << l })+ \"]\" { raise ParseError, 'invalid inline note' unless @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref }") + Rules[:_InlineNote] = rule_info("InlineNote", "&{ notes? } \"^[\" @StartList:a (!\"]\" Inline:l { a << l })+ \"]\" { if @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref end }") Rules[:_Notes] = rule_info("Notes", "(Note | SkipBlock)*") Rules[:_RawNoteBlock] = rule_info("RawNoteBlock", "@StartList:a (!@BlankLine !RawNoteReference OptionallyIndentedLine:l { a << l })+ < @BlankLine* > { a << text } { a }") Rules[:_CodeFence] = rule_info("CodeFence", "&{ github? } Ticks3 (@Sp StrChunk:format)? @Sp @Newline? < ((!\"`\" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }") diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb index 136b509b1d..f2542f5157 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -1002,16 +1002,10 @@ def test_parse_note_inline assert_equal expected, doc end - def test_parse_note_inline_in_reference_label_after_reuse + def test_parse_note_inline_in_reference_label @parser.notes = true - parse "Some text. ^[With a footnote]" - - error = assert_raise RDoc::Markdown::ParseError do - parse "[foo ^[note]]: /url\n" - end - - assert_equal 'invalid inline note', error.message + assert_kind_of RDoc::Markup::Document, parse("[foo ^[note]]: /url\n") end def test_parse_note_no_notes