From b7b12c270a146648d3574ef99c8c7145fa01b7db Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 24 May 2026 12:51:42 +0200 Subject: [PATCH 1/2] Resolve markdown crash for note references in labels (#1712) Fixes #654. Note references can be parsed during the reference-gathering pass, before footnote ordering is initialized. Previously this could crash with a NoMethodError for malformed reference labels. `RDoc::Markdown.parse("[[^0]\n")` Before the fix, it raised: ```ruby NoMethodError: undefined method `<<' for nil .../lib/rdoc/markdown.rb:...:in `note_for' ``` Or: `RDoc::Markdown.parse("[foo[^1]bar]\n\n[^1]: footnote\n")` --- lib/rdoc/markdown.kpeg | 2 ++ lib/rdoc/markdown.rb | 2 ++ test/rdoc/rdoc_markdown_test.rb | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index 0e15a604f4..893204151f 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -403,6 +403,8 @@ # the note order list for proper display at the end of the document. def note_for ref + return unless @note_order + @note_order << ref label = @note_order.length diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index c01cab0096..4326fb6549 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -788,6 +788,8 @@ def note label # the note order list for proper display at the end of the document. def note_for ref + return unless @note_order + @note_order << ref label = @note_order.length diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb index 646f12a999..5d432c6cc5 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -1002,6 +1002,29 @@ def test_parse_note_inline assert_equal expected, doc end + def test_parse_note_invalid_reference + @parser.notes = true + + assert_kind_of RDoc::Markup::Document, parse("[[^0]\n") + end + + def test_parse_note_reference_in_reference_label + @parser.notes = true + + doc = parse <<~MD + [foo[^1]bar] + + [^1]: footnote + MD + + expected = doc( + para("[foo{*1}[rdoc-label:foottext-1:footmark-1]bar]"), + rule(1), + para("{^1}[rdoc-label:footmark-1:foottext-1] footnote")) + + assert_equal expected, doc + end + def test_parse_note_no_notes @parser.notes = false From 0a388a7eba8099bc57910c5e443a89299f28afde Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 24 May 2026 19:47:10 +0200 Subject: [PATCH 2/2] Ignore inline notes during Markdown reference parsing (#1713) Inline notes inside reference labels can be parsed during the reference-gathering pass, before footnote ordering is initialized. `RDoc::Markdown.parse("[foo ^[note]]: /url\n")` This will crash a Markdown parser with a `NoMethodError`. This change treats inline-note creation as a no-op until note ordering exists. --- lib/rdoc/markdown.kpeg | 8 +++++--- lib/rdoc/markdown.rb | 12 +++++++----- test/rdoc/rdoc_markdown_test.rb | 6 ++++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index 893204151f..24a9a053f8 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -1236,10 +1236,12 @@ InlineNote = &{ notes? } @StartList:a ( !"]" Inline:l { a << l } )+ "]" - { ref = [:inline, @note_order.length] - @footnotes[ref] = paragraph a + { if @note_order + 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 4326fb6549..af829d2dd0 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -15478,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 })+ "]" { if @note_order ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref end } def _InlineNote _save = self.pos @@ -15569,10 +15569,12 @@ def _InlineNote self.pos = _save break end - @result = begin; ref = [:inline, @note_order.length] - @footnotes[ref] = paragraph a + @result = begin; if @note_order + ref = [:inline, @note_order.length] + @footnotes[ref] = paragraph a - note_for ref + note_for ref + end ; end _tmp = true unless _tmp @@ -16843,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 })+ \"]\" { 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 5d432c6cc5..d98d9ede3f 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -1025,6 +1025,12 @@ def test_parse_note_reference_in_reference_label assert_equal expected, doc end + def test_parse_note_inline_in_reference_label + @parser.notes = true + + assert_kind_of RDoc::Markup::Document, parse("[foo ^[note]]: /url\n") + end + def test_parse_note_no_notes @parser.notes = false