From 399636eaef370276be5e799be924b917b776f13a Mon Sep 17 00:00:00 2001 From: st0012 Date: Thu, 26 Feb 2026 10:13:00 +0000 Subject: [PATCH 1/3] Fix markdown table parser consuming lines without pipes as table rows The TableRow rule in the PEG grammar allowed rows with zero pipe characters (TableItem2*). This caused lines like `
` immediately following a table to be parsed as single-cell table rows, producing spurious `
` in the rendered HTML. Change TableItem2* to TableItem2+ so that rows not starting with `|` must contain at least one `|` to be recognized as table rows. --- lib/rdoc/markdown.kpeg | 2 +- lib/rdoc/markdown.rb | 23 +++++++++++++++-------- test/rdoc/rdoc_markdown_test.rb | 24 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index cde3e927f4..cdf807c33a 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -1270,7 +1270,7 @@ Table = &{ github? } TableHead = TableItem2+:items "|"? @Newline { items } -TableRow = ( ( TableItem:item1 TableItem2*:items { [item1, *items] } ):row | TableItem2+:row ) "|"? @Newline +TableRow = ( ( TableItem:item1 TableItem2+:items { [item1, *items] } ):row | TableItem2+:row ) "|"? @Newline { row } TableItem2 = "|" TableItem TableItem = < /(?:\\.|[^|\n])+/ > diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index 7e4adcefc3..321c365348 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -15937,7 +15937,7 @@ def _TableHead return _tmp end - # TableRow = ((TableItem:item1 TableItem2*:items { [item1, *items] }):row | TableItem2+:row) "|"? @Newline { row } + # TableRow = ((TableItem:item1 TableItem2+:items { [item1, *items] }):row | TableItem2+:row) "|"? @Newline { row } def _TableRow _save = self.pos @@ -15954,14 +15954,21 @@ def _TableRow self.pos = _save2 break end + _save3 = self.pos _ary = [] - while true - _tmp = apply(:_TableItem2) - _ary << @result if _tmp - break unless _tmp + _tmp = apply(:_TableItem2) + if _tmp + _ary << @result + while true + _tmp = apply(:_TableItem2) + _ary << @result if _tmp + break unless _tmp + end + _tmp = true + @result = _ary + else + self.pos = _save3 end - _tmp = true - @result = _ary items = @result unless _tmp self.pos = _save2 @@ -16666,7 +16673,7 @@ def _DefinitionListDefinition Rules[:_CodeFence] = rule_info("CodeFence", "&{ github? } Ticks3 (@Sp StrChunk:format)? Spnl < ((!\"`\" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }") Rules[:_Table] = rule_info("Table", "&{ github? } TableHead:header TableLine:line TableRow+:body { table = RDoc::Markup::Table.new(header, line, body) parse_table_cells(table) }") Rules[:_TableHead] = rule_info("TableHead", "TableItem2+:items \"|\"? @Newline { items }") - Rules[:_TableRow] = rule_info("TableRow", "((TableItem:item1 TableItem2*:items { [item1, *items] }):row | TableItem2+:row) \"|\"? @Newline { row }") + Rules[:_TableRow] = rule_info("TableRow", "((TableItem:item1 TableItem2+:items { [item1, *items] }):row | TableItem2+:row) \"|\"? @Newline { row }") Rules[:_TableItem2] = rule_info("TableItem2", "\"|\" TableItem") Rules[:_TableItem] = rule_info("TableItem", "< /(?:\\\\.|[^|\\n])+/ > { text.strip.gsub(/\\\\([|])/, '\\1') }") Rules[:_TableLine] = rule_info("TableLine", "((TableAlign:align1 TableAlign2*:aligns {[align1, *aligns] }):line | TableAlign2+:line) \"|\"? @Newline { line }") diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb index eb7c487f27..c18127e211 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -1310,6 +1310,30 @@ def test_gfm_table_with_links_and_code assert_equal expected, doc end + def test_gfm_table_does_not_consume_following_line_without_pipe + doc = parse <<~MD + | Command | Shows | + |---------|-------| + | ri File | Document for Ruby class File. | +
+ + Following paragraph. + MD + + head = %w[Command Shows] + align = [nil, nil] + body = [ + ['ri File', 'Document for Ruby class File.'], + ] + expected = doc( + @RM::Table.new(head, align, body), + para('
'), + para('Following paragraph.') + ) + + assert_equal expected, doc + end + def test_gfm_table_with_backslashes_in_code_spans doc = parse <<~MD Plain text: `$\\\\` and `$\\\\ ` should work. From 60820a6bbe447ad4f0dc0bc43392d7cbb628378e Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 27 Feb 2026 23:08:28 +0000 Subject: [PATCH 2/3] Apply the same fix to TableLine: require at least one pipe Change TableAlign2* to TableAlign2+ in the TableLine rule for consistency with the TableRow fix. A separator line without any pipe character would conflict with horizontal rule parsing anyway. --- lib/rdoc/markdown.kpeg | 2 +- lib/rdoc/markdown.rb | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg index cdf807c33a..d95a88a823 100644 --- a/lib/rdoc/markdown.kpeg +++ b/lib/rdoc/markdown.kpeg @@ -1276,7 +1276,7 @@ TableItem2 = "|" TableItem TableItem = < /(?:\\.|[^|\n])+/ > { text.strip.gsub(/\\([|])/, '\1') } -TableLine = ( ( TableAlign:align1 TableAlign2*:aligns {[align1, *aligns] } ):line | TableAlign2+:line ) "|"? @Newline +TableLine = ( ( TableAlign:align1 TableAlign2+:aligns {[align1, *aligns] } ):line | TableAlign2+:line ) "|"? @Newline { line } TableAlign2 = "|" @Sp TableAlign TableAlign = < /:?-+:?/ > @Sp diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index 321c365348..e4d0ae9ff6 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -16084,7 +16084,7 @@ def _TableItem return _tmp end - # TableLine = ((TableAlign:align1 TableAlign2*:aligns {[align1, *aligns] }):line | TableAlign2+:line) "|"? @Newline { line } + # TableLine = ((TableAlign:align1 TableAlign2+:aligns {[align1, *aligns] }):line | TableAlign2+:line) "|"? @Newline { line } def _TableLine _save = self.pos @@ -16101,14 +16101,21 @@ def _TableLine self.pos = _save2 break end + _save3 = self.pos _ary = [] - while true - _tmp = apply(:_TableAlign2) - _ary << @result if _tmp - break unless _tmp + _tmp = apply(:_TableAlign2) + if _tmp + _ary << @result + while true + _tmp = apply(:_TableAlign2) + _ary << @result if _tmp + break unless _tmp + end + _tmp = true + @result = _ary + else + self.pos = _save3 end - _tmp = true - @result = _ary aligns = @result unless _tmp self.pos = _save2 @@ -16676,7 +16683,7 @@ def _DefinitionListDefinition Rules[:_TableRow] = rule_info("TableRow", "((TableItem:item1 TableItem2+:items { [item1, *items] }):row | TableItem2+:row) \"|\"? @Newline { row }") Rules[:_TableItem2] = rule_info("TableItem2", "\"|\" TableItem") Rules[:_TableItem] = rule_info("TableItem", "< /(?:\\\\.|[^|\\n])+/ > { text.strip.gsub(/\\\\([|])/, '\\1') }") - Rules[:_TableLine] = rule_info("TableLine", "((TableAlign:align1 TableAlign2*:aligns {[align1, *aligns] }):line | TableAlign2+:line) \"|\"? @Newline { line }") + Rules[:_TableLine] = rule_info("TableLine", "((TableAlign:align1 TableAlign2+:aligns {[align1, *aligns] }):line | TableAlign2+:line) \"|\"? @Newline { line }") Rules[:_TableAlign2] = rule_info("TableAlign2", "\"|\" @Sp TableAlign") Rules[:_TableAlign] = rule_info("TableAlign", "< /:?-+:?/ > @Sp { text.start_with?(\":\") ? (text.end_with?(\":\") ? :center : :left) : (text.end_with?(\":\") ? :right : nil) }") Rules[:_DefinitionList] = rule_info("DefinitionList", "&{ definition_lists? } DefinitionListItem+:list { RDoc::Markup::List.new :NOTE, *list.flatten }") From a757b753c806a0e2a88a4cef68f181b0fdc72381 Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 27 Feb 2026 23:28:54 +0000 Subject: [PATCH 3/3] Add test for TableLine separator without pipe Verify that a separator line with no pipe character does not form a valid table, matching the TableLine rule change. --- test/rdoc/rdoc_markdown_test.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb index c18127e211..608974d7c9 100644 --- a/test/rdoc/rdoc_markdown_test.rb +++ b/test/rdoc/rdoc_markdown_test.rb @@ -1334,6 +1334,19 @@ def test_gfm_table_does_not_consume_following_line_without_pipe assert_equal expected, doc end + def test_gfm_table_separator_without_pipe_does_not_form_table + doc = parse <<~MD + | Command | Shows | + --------- + | ri File | Document for Ruby class File. | + MD + + # The separator line has no pipe, so this should NOT parse as a table. + # "| Command | Shows |" becomes a paragraph, "---" becomes a paragraph, + # and "| ri File | ..." becomes a paragraph. + assert(doc.parts.none? { |part| part.is_a?(@RM::Table) }) + end + def test_gfm_table_with_backslashes_in_code_spans doc = parse <<~MD Plain text: `$\\\\` and `$\\\\ ` should work.