fix(markdown): prevent currency values from being parsed as inline math - #463
Conversation
Disable singleDollarTextMath so that dollar signs in ordinary prose (e.g. "$3.50" or "costs $9.99 ... $19.99 per month") are no longer interpreted as KaTeX inline-math delimiters. This caused currency values to render with italic styling and collapsed spacing. Math content using $...$ syntax still works via $$...$$ (display) and \(...\) / \[...\] (inline, normalized to $$..$$).
normalizeMathDelimiters now converts \(...\) to ... instead of $...$ so inline math still works after singleDollarTextMath is disabled. Update comments and add regression tests for currency preservation and \(...\) math normalization.
|
Thanks for this, and for taking the time to write up the reasoning so clearly — the diagnosis is correct and I could reproduce it exactly. I parsed a few samples with the versions this repo actually has installed ( One thing your description undersells: the same bug eats shell variables, which for a coding workbench are much more common than currency. That's honestly the strongest argument for the change here, and worth putting in the PR description. I also want to give credit to the And it correctly stays inline — Local gates on your branch are all green: prettier, So — direction agreed. There's one thing I'd like fixed before merge, plus a couple of notes. 🔴 Needs a fix: multi-line
|
Do not collapse inner newlines. Pad a start-of-block multi-line paren formula with a ZWSP so $$ is not a fence, and keep the closer on the last content line. Parse-level tests pin currency, $x$, $HOME, and the multi-line cases. Lone $ no longer warms KaTeX.
|
Pushed a follow-up. The multi-line
Lone Comments you flagged are cleaned up. |
|
Thanks for turning this around so fast — and for not taking the shortcut. You adopted every point, and the two things I was most worried about are handled correctly: Blockquotes, So the approach is right. But I ran the position logic across the rest of the container grammar and there's more surface than either of us thought. Everything below is measured against 🔴 R1 — the closer needs the same treatment as the openerOnly the opening That renders as a literal 🔴 R2 — the opener prefix grammar is narrower than CommonMark'sThe last one is the realistic shape — a bulleted point whose formula wraps. The common thread: the regex only accepts prefixes built from container markers with exactly one trailing space, so bare indentation is never recognised, and that's exactly what a continuation line looks like. 🔴 R3 — CR / CRLF
🔴 R4 — the prefix regex backtracks exponentiallyMeasured on
Clean doubling every 2 markers. In 🟡 One note on the ZWSPIt's a reasonable trick and I'm not asking you to drop it, but it isn't inert — it's a real character every downstream matcher sees. Emphasis is fine in the shapes you pad (I checked Only the line-start occurrence is padded, so the label normalises to On the approach — I need to walk back my round-1 suggestionI suggested "do it at the parser level with a remark plugin visiting text nodes." That doesn't work, and I should have checked before suggesting it. CommonMark eats the escaping backslashes, so by the time a text node exists there is nothing left to key on: And markdown-active bodies shred across nodes — A micromark syntax extension for So: your layer is the right layer for how this app currently drives Streamdown. It's incomplete, not wrong. What's left is R1–R4: canonicalise CR/CRLF before any offset logic, give the closer the same handling as the opener, and make the position test both complete and linear. Totally fine if that's more than you signed up for — say the word and I'll take it from here and credit you on the commit. If you'd rather finish it, I'll review promptly. Either way, thanks for the care you've put into this one. 🙏 |
Move a prefix-only last line after $$ so the closer cannot open a flow fence. Walk CommonMark prefixes in one pass (+, indent, continuation). Fold CR/CRLF to LF first.
|
Pushed another follow-up. R1: ZWSP before the closer ended up inside the formula, and after it still opened a fence. If the last line is only a container prefix I move that line after R2: the prefix walk now accepts R3: CR and CRLF get folded to LF before any offset work. R4: that walk is a single pass now, no regex. I left a note on the opener ZWSP about link reference labels. |
|
Thanks — and genuinely, this round is good work. R1–R4 are all properly fixed, and I verified each one shape by shape against
Gates are green: prettier, eslint, tsc, your three test files (37 tests), and the full suite at 4097/4098 — the one failure is an unrelated 5s-timeout flake in I owe you a correction before anything else: I was about to suggest two one-line fixes, and I checked them first — both are wrong. Removing the What's still openN1 — N2 — the trailing-newline peel splits paragraphs when prose follows. The peeled Different mechanism from R5 — this newline lives in N3 — the CR fix introduced an inline-code regression. The fold runs before the code masking, and the inline-code mask is R5 (still open) — the moved closer line leaves a line of only the container prefix, i.e. a blank line, so B1 (still open) — container prefixes ≥ 4 columns still corrupt: ordered item ≥ 10, 2+ level nested lists. Where I think this leaves usRound 1 found one defect. Fixing it surfaced four. Fixing those surfaced five. And the two obvious one-liners for this round are both unsafe. I don't think that's carelessness on your part — I think it's the layer. So my suggestion: cut the multi-line case out of this PR and let's land the rest. Concretely — rewrite Two things to be precise about, because I checked and my first read was too rosy:
(For the record: multi-line That keeps everything this PR is actually for: If you'd rather not do another push, say the word and I'll do the descope myself and land it with you credited. You've put four solid rounds into this and I'd like it to ship. 🙏 Whatever lands, it should carry AST-shape regressions rather than only value checks — the current parse tests never assert paragraph or list-spread structure, and the linearity test is a single wall-clock |
|
I went ahead and fixed the remaining five myself rather than sending you round five — sorry for the length of this thread. 🙏 → Adam-Dalloul#1 (opened against your branch; "Allow edits by maintainers" is off, so I couldn't push directly). Merging it updates this PR in place and I'll merge #463 straight after. The short version: every remaining defect was the same question wearing a different hat — is this line a container prefix, or is it TeX? — and a single line can't answer it. So the fix stops asking. The closing Your ZWSP instinct was right, by the way. The part neither of us checked: KaTeX doesn't ignore U+200B. It lexes as a Everything you built stays: Full suite green, 307 files / 4109 tests. Thanks for sticking with four rounds of increasingly obscure markdown trivia — the |
|
Landed — this is done. ✅ Correcting my earlier note: rather than merging the follow-up into your branch first, I merged this PR as-is and then applied the remaining fixes directly on top, so Final state on The one thing deliberately left alone, pre-existing and unchanged by any of this: Thanks for the patience across four rounds — good contribution. 🙏 |
Follow-up to #463. The `\(...\)` -> `$$...$$` rewrite still had to keep `$$` off the start of a line's block content, and the guards for that were asking a question a single line cannot answer — container prefix, or TeX? Marker-shaped TeX was pushed out of formulas, prefix-only closing lines split paragraphs and loosened list items, CR/CRLF corrupted inline code, and >= 4-column prefixes were unguarded. Replaces the classifier: the closing `$$` is padded unconditionally (that pad lands inside the formula, so no decision is needed) and stripped back out after parsing, before KaTeX — which lexes U+200B as a `textord` and would otherwise shift the spacing of a terminal operator.
Single
$...$was being treated as math, so prices and shell vars like$HOMElooked broken.This turns
singleDollarTextMathoff. Real math still works via$$...$$and\(...\).Follow-up after review:
\(...\)at the start of a block no longer drops the first line$9.99,$x$,$HOME,$1$does not load KaTeX