Summary
parse_markdown silently deletes text when a <u> underline marker is left unclosed and another <u> appears later in the same line. The two <u> markers are matched against each other as an emphasis pair and consumed, taking the text around them with them.
Reproduction (against the real parser)
| Input |
Expected (literal round-trip, like an unclosed *) |
Actual |
<u><u> |
<u><u> |
empty line — all text gone |
<u>a<u> |
<u>a<u> |
a (both <u> deleted) |
a <u>word<u> b |
a <u>word<u> b |
a word b (both <u> deleted) |
assert_eq!(parse_markdown("<u><u>").unwrap().raw_text(), "<u><u>\n"); // fails: left is "\n"
Root cause
<u> is tokenized as an UnderlineStart delimiter (crates/markdown_parser/src/markdown_parser.rs, the InlineToken::Delimiter arm) and pushed onto the shared delimiter stack with can_close set from the right-flanking rules (Delimiter::new). As a result process_emphasis sees two <u> markers as a matchable opener/closer pair — can_open_for passes because they share kind == DelimiterKind::UnderlineStart — and the opener.kind == UnderlineStart branch in process_emphasis consumes both markers and removes their text nodes.
Underline is only ever supposed to be closed by an explicit </u>, which is handled separately by parse_underline (it never goes through can_open_for). So pairing two <u> openers through the emphasis path is always wrong; an unclosed <u> should round-trip to literal text, exactly like an unclosed *.
Impact
parse_markdown is the main renderer for agent output and markdown-viewer content, so a partial/streamed <u> or two <u> tags silently drop the literal text. This is data loss in rendered output, not a crash.
Notes
- Crate-scoped and reproducible with a pure unit test (no UI needed).
- I have a small fix ready (make
can_open_for reject UnderlineStart; ~8 lines) with regression tests for the cases above, and all existing markdown_parser tests still pass. Happy to open a PR once this is labeled ready-to-implement.
Operating system
All (platform-independent parser logic)
Summary
parse_markdownsilently deletes text when a<u>underline marker is left unclosed and another<u>appears later in the same line. The two<u>markers are matched against each other as an emphasis pair and consumed, taking the text around them with them.Reproduction (against the real parser)
*)<u><u><u><u><u>a<u><u>a<u>a(both<u>deleted)a <u>word<u> ba <u>word<u> ba word b(both<u>deleted)Root cause
<u>is tokenized as anUnderlineStartdelimiter (crates/markdown_parser/src/markdown_parser.rs, theInlineToken::Delimiterarm) and pushed onto the shared delimiter stack withcan_closeset from the right-flanking rules (Delimiter::new). As a resultprocess_emphasissees two<u>markers as a matchable opener/closer pair —can_open_forpasses because they sharekind == DelimiterKind::UnderlineStart— and theopener.kind == UnderlineStartbranch inprocess_emphasisconsumes both markers and removes their text nodes.Underline is only ever supposed to be closed by an explicit
</u>, which is handled separately byparse_underline(it never goes throughcan_open_for). So pairing two<u>openers through the emphasis path is always wrong; an unclosed<u>should round-trip to literal text, exactly like an unclosed*.Impact
parse_markdownis the main renderer for agent output and markdown-viewer content, so a partial/streamed<u>or two<u>tags silently drop the literal text. This is data loss in rendered output, not a crash.Notes
can_open_forrejectUnderlineStart; ~8 lines) with regression tests for the cases above, and all existingmarkdown_parsertests still pass. Happy to open a PR once this is labeledready-to-implement.Operating system
All (platform-independent parser logic)