Describe the bug
Input that ends on a blockquote marker while a table is open inside that quote raises
IndexError: string index out of range.
Two block rules are affected, both reached as terminator rules from rules_block/table.py:190:
| rule |
line |
rules_block/html_block.py |
if state.src[pos] != "<": |
rules_block/heading.py |
`ch: str |
Both compute pos = state.bMarks[startLine] + state.tShift[startLine], which equals len(state.src)
for the empty final line a trailing > produces. In markdown-it (JS) the equivalent is
state.src.charCodeAt(pos), which returns NaN out of range instead of throwing — the port hazard
tracked in #190.
Sibling rules already defend against exactly this: hr.py and blockquote.py wrap the same index in
try: ... except IndexError: return False (added for #185 / #204). html_block.py and heading.py
appear to have been missed.
html_block shadows heading because it runs first, so disabling html moves the traceback rather
than fixing it.
Reproduce the bug
from markdown_it import MarkdownIt
# html_block.py:47
MarkdownIt().enable("table").parse("> | a | b |\n> |---|---|\n>")
# heading.py:22 (same input, html_block bails at its options check first)
MarkdownIt("commonmark", {"html": False}).enable("table").parse("> | a | b |\n> |---|---|\n>")
Both raise:
IndexError: string index out of range
Needs table enabled, so gfm-like and js-default are affected; plain commonmark is not, since
without the table rule the terminator rules are never run on that line.
Variants (all with MarkdownIt().enable("table"))
| input |
result |
"> | a | b |\n> |---|---|\n>" |
IndexError |
"> | a | b |\n> |---|---|\n> " (marker + space) |
IndexError |
"> > | a | b |\n> > |---|---|\n> >" (nested) |
IndexError |
"> | a | b |\n> |---|---|\n> | 1 | 2 |\n>" (body row, then bare marker) |
IndexError |
"> | a | b |\n> |---|---|\n>\n" (trailing newline) |
ok |
"> | a | b |\n> |---|---|\n> | 1 | 2 |" (complete) |
ok |
"| a | b |\n|---|---|" (no blockquote) |
ok |
"> text\n>" (no table) |
ok |
A trailing newline makes it disappear, which is why this is easy to miss in a document renderer and
hard to avoid in an incremental one. I hit it rendering a partially-streamed LLM reply with
rich.markdown — > |---|---|\n> is not markdown anyone writes, but it exists for a few
milliseconds every time a quoted table is streamed a token at a time. rich parses in
Markdown.__init__, so it raises at construction and takes down the caller.
Suggested fix
The guard the sibling rules already use:
--- a/markdown_it/rules_block/html_block.py
+++ b/markdown_it/rules_block/html_block.py
- if state.src[pos] != "<":
- return False
+ try:
+ if state.src[pos] != "<":
+ return False
+ except IndexError:
+ return False
--- a/markdown_it/rules_block/heading.py
+++ b/markdown_it/rules_block/heading.py
- ch: str | None = state.src[pos]
-
- if ch != "#" or pos >= maximum:
+ if pos >= maximum or state.src[pos] != "#":
return False
Happy to open a PR with these plus tests if the approach looks right — though if #190 is still the
preferred direction, that would fix this class of bug at the source and these two rules are simply
further evidence for it.
List your environment
markdown-it-py 4.2.0 (the two lines above are unchanged on master as of this writing)
- Python 3.12, Linux
- No optional dependencies needed to reproduce
Describe the bug
Input that ends on a blockquote marker while a table is open inside that quote raises
IndexError: string index out of range.Two block rules are affected, both reached as terminator rules from
rules_block/table.py:190:rules_block/html_block.pyif state.src[pos] != "<":rules_block/heading.pyBoth compute
pos = state.bMarks[startLine] + state.tShift[startLine], which equalslen(state.src)for the empty final line a trailing
>produces. In markdown-it (JS) the equivalent isstate.src.charCodeAt(pos), which returnsNaNout of range instead of throwing — the port hazardtracked in #190.
Sibling rules already defend against exactly this:
hr.pyandblockquote.pywrap the same index intry: ... except IndexError: return False(added for #185 / #204).html_block.pyandheading.pyappear to have been missed.
html_blockshadowsheadingbecause it runs first, so disablinghtmlmoves the traceback ratherthan fixing it.
Reproduce the bug
Both raise:
Needs
tableenabled, sogfm-likeandjs-defaultare affected; plaincommonmarkis not, sincewithout the table rule the terminator rules are never run on that line.
Variants (all with
MarkdownIt().enable("table"))"> | a | b |\n> |---|---|\n>""> | a | b |\n> |---|---|\n> "(marker + space)"> > | a | b |\n> > |---|---|\n> >"(nested)"> | a | b |\n> |---|---|\n> | 1 | 2 |\n>"(body row, then bare marker)"> | a | b |\n> |---|---|\n>\n"(trailing newline)"> | a | b |\n> |---|---|\n> | 1 | 2 |"(complete)"| a | b |\n|---|---|"(no blockquote)"> text\n>"(no table)A trailing newline makes it disappear, which is why this is easy to miss in a document renderer and
hard to avoid in an incremental one. I hit it rendering a partially-streamed LLM reply with
rich.markdown—> |---|---|\n>is not markdown anyone writes, but it exists for a fewmilliseconds every time a quoted table is streamed a token at a time.
richparses inMarkdown.__init__, so it raises at construction and takes down the caller.Suggested fix
The guard the sibling rules already use:
Happy to open a PR with these plus tests if the approach looks right — though if #190 is still the
preferred direction, that would fix this class of bug at the source and these two rules are simply
further evidence for it.
List your environment
markdown-it-py4.2.0 (the two lines above are unchanged onmasteras of this writing)