Skip to content

fix(remend): guard incomplete HTML tag handler against math blocks - #617

Open
torsello wants to merge 1 commit into
vercel:mainfrom
torsello:fix/html-tag-math-guard
Open

fix(remend): guard incomplete HTML tag handler against math blocks#617
torsello wants to merge 1 commit into
vercel:mainfrom
torsello:fix/html-tag-math-guard

Conversation

@torsello

@torsello torsello commented Sep 10, 2026

Copy link
Copy Markdown

Description

handleIncompleteHtmlTag guarded against code blocks but never against math, so an ordinary
comparison inside a math expression matched the incomplete-tag pattern and deleted everything
from the < to the end of the string.

remend('Intro\n\n$$\nI = \\sum_{j<k} p_j\n$$\n\nTAIL')
// before → 'Intro\n\n$$\nI = \\sum_{j\n$$'   ← TAIL gone, math truncated
// after  → unchanged

Because the katex handler (priority 70) runs after htmlTags (priority 10), the now-orphaned
$$ was auto-closed, so KaTeX received unterminated input and rendered a ParseError in
--color-muted-foreground. The net effect for the reader is a message that silently ends
mid-formula in grey text, with no error surfaced anywhere.

remend already implements and exports isWithinMathBlock, and the emphasis handlers already
use it — htmlTags was simply never covered.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Related Issues

Related to #616

Deliberately not Fixes — this covers the math half of that issue. See Additional Notes.

Changes Made

  • Added an isWithinMathBlock guard to handleIncompleteHtmlTag, gated behind the same
    hasMathDelimiters fast path the emphasis handlers use, so the O(n) scan is skipped
    entirely for text with no math delimiters.

  • Changed the handler to walk forward through candidates instead of bailing out on the
    first guarded one.
    This is the non-obvious half. incompleteHtmlTagPattern is
    leftmost-matching and its [^>]*$ tail means the match always runs to the end of the
    string — so every later < that starts a tag name is an equally valid candidate. Simply
    returning text when the first match falls inside math would trade one bug for another:

    remend('$x<y$ then <div')
    // guard-only → '$x<y$ then <div'   ← the genuine incomplete tag now survives. wrong.
    // this PR    → '$x<y$ then'        ← correct

    The same walk-forward now applies to code blocks, which fixes a smaller unreported case
    on its own: a < inside a fenced block no longer masks a real incomplete tag after it.

  • Added 4 test cases to packages/remend/__tests__/html-tags.test.ts.

  • Added a patch changeset for remend.

Testing

  • All existing tests pass
  • Added new tests for the changes
  • Manually tested the changes

New cases cover block $$, inline $, both LaTeX delimiter forms (\(…\) and \[…\]), and
the walk-forward behaviour. I confirmed all four fail on main before the fix and pass after —
the walk-forward case in particular fails against a guard-only implementation too.

Manual verification against the exact reproductions from the issue:

Input Before After
Intro\n\n$$\nI = \sum_{j<k} p_j\n$$\n\nTAIL truncated at \sum_{j unchanged
inline $$A_{j<k}$$ more\n\nTAIL inline $$A_{j$$ unchanged
Hello <div Hello Hello (unchanged behaviour)

Test Coverage

remend: 493 tests across 26 files, all passing.
Full monorepo suite: 1172 tests across 85 files, 6/6 turbo tasks green.
biome check reports no findings on both touched files.

Note: tsc --noEmit reports two TS18048 errors in packages/remend/src/strikethrough-handler.ts.
I verified these are present on main and unrelated to this change.

Screenshots/Demos

Not applicable — no visual surface changes. The before/after is shown as code above.

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have created a changeset (pnpm changeset)

Documentation left unchecked: this is a behavioural fix with no public API change, so there is
nothing in the docs to update. Happy to add a note to the termination docs if you'd like one.

Changeset

  • I have created a changeset for these changes

.changeset/html-tag-math-guard.mdpatch bump for remend.

Additional Notes

What this PR intentionally leaves out. #616 raises a second, broader problem: the handler
also fires on text that is already complete, so ordinary prose like the loop runs while i<n
still loses its remainder. That one is not a missing guard — it needs an explicit signal,
because remend(text) currently cannot distinguish a streaming tail from settled text, and
every tail-oriented heuristic keeps firing after the stream ends.

The issue suggests an option or a mode argument mirroring Streamdown's mode="static".
That is a public API decision affecting more than this handler, so I did not want to make it
unilaterally in a first contribution. Happy to open a follow-up if you want to shape the API —
or to fold it into this PR if you already know which direction you'd prefer.

@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@torsello is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@torsello
torsello force-pushed the fix/html-tag-math-guard branch from 73c27f6 to 72a16f3 Compare September 10, 2026 12:40
`handleIncompleteHtmlTag` checked `isInsideCodeBlock` but never the
`isWithinMathBlock` guard the emphasis handlers already use. An ordinary
comparison inside math -- `$$ I = \sum_{j<k} p_j $$` -- therefore matched
the incomplete-tag pattern and deleted everything from the `<` to the end
of the string. The katex handler then auto-closed the orphaned `$$`, so
KaTeX received truncated input and rendered a parse error in muted text
while the rest of the message never reached the DOM.

Since the pattern is leftmost-matching and always runs to the end of the
string, every later `<` that starts a tag name is an equally valid
candidate. The handler now walks forward past candidates that sit inside
code or math rather than bailing out, so a genuine incomplete tag that
follows a math expression is still stripped.

Refs vercel#616
@torsello
torsello force-pushed the fix/html-tag-math-guard branch from 72a16f3 to dd1ddec Compare September 10, 2026 13:04
lofcz added a commit to lofcz/streamdown-ng that referenced this pull request Sep 11, 2026
Port vercel/streamdown#617 by @torsello: skip < candidates inside
math blocks and walk forward to later candidates instead of bailing
out, so a comparison like \sum_{j<k} no longer swallows the message
tail while genuine incomplete tags are still stripped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant