Bug Description
remend (applied whenever mode="streaming" and parseIncompleteMarkdown are at their defaults) skips intraword * delimiters when counting emphasis, treating them as unable to close an italic run. But per CommonMark, * can close emphasis intraword: *foo*bar parses as <em>foo</em>bar (micromark agrees — rendering the same string with mode="static" produces exactly that).
Because remend miscounts the run as unclosed, it appends a spurious * to the text. And since remend runs on every render while mode="streaming" — including renders after the stream has completed — the stray * ends up permanently in the final output, not just in a transient mid-stream state.
This is especially painful for Korean/Japanese content, where particles/suffixes attach directly to the emphasized word (e.g. *단어*로), so LLM output hits this pattern constantly. It corrupts the message whenever the number of intraword closers in a block is odd (even counts cancel out in remend's counter, which is why the bug looks intermittent).
Steps to Reproduce
- Render
<Streamdown>{'*foo*bar'}</Streamdown> (defaults: mode="streaming", parseIncompleteMarkdown on)
- Compare with
<Streamdown mode="static">{'*foo*bar'}</Streamdown>
- The streaming-mode output has a literal trailing
* that never goes away
Calling remend directly shows the corruption at the string level:
import remend from 'remend';
remend('*foo*bar');
// => '*foo*bar*' (expected: unchanged — the input is complete, valid CommonMark)
Expected Behavior
<em>foo</em>bar — identical to mode="static" and to CommonMark reference implementations. Complete, valid markdown should pass through remend unchanged.
Actual Behavior
<em>foo</em>bar* — a spurious literal * is appended, and it persists after streaming finishes.
renderToStaticMarkup(<Streamdown>{'*foo*bar'}</Streamdown>);
// => <div><p><em>foo</em>bar*</p></div>
renderToStaticMarkup(<Streamdown mode="static">{'*foo*bar'}</Streamdown>);
// => <div><p><em>foo</em>bar</p></div>
More real-world examples (all complete texts, all corrupted the same way):
| input |
streaming-mode text |
expected text |
this is *real*ly good |
this is really good* |
this is really good |
이것은 *기울임*으로 표시 |
이것은 기울임으로 표시* |
이것은 기울임으로 표시 |
Code Sample
import { renderToStaticMarkup } from 'react-dom/server';
import { Streamdown } from 'streamdown';
// spurious trailing "*"
renderToStaticMarkup(<Streamdown>{'*foo*bar'}</Streamdown>);
Streamdown Version
2.5.0 (remend 1.3.0; also reproduces against current main)
React Version
18.2.0
Node.js Version
24.18.0
Browser(s)
Not applicable (Node.js)
Operating System
macOS
Additional Context
The skip happens in packages/remend/src/emphasis-handlers.ts — the "Skip if flanked by word chars on both sides" checks treat an intraword * as a non-delimiter. That matches CommonMark's rule for _, but not for *: an intraword * is both left- and right-flanking, so it can open and close emphasis (cf. the CommonMark spec example 5*6*78 → 5<em>6</em>78).
So when a run has a valid opener (e.g. after whitespace) and its closer happens to be intraword, remend's open-counter stays at 1 and the completion logic appends * at the end of the block. A fix could be to count intraword * as a potential closer whenever a run is currently open, mirroring micromark's delimiter matching.
Workaround we're using in the meantime: remend={{ italic: false }}.
Bug Description
remend(applied whenevermode="streaming"andparseIncompleteMarkdownare at their defaults) skips intraword*delimiters when counting emphasis, treating them as unable to close an italic run. But per CommonMark,*can close emphasis intraword:*foo*barparses as<em>foo</em>bar(micromark agrees — rendering the same string withmode="static"produces exactly that).Because remend miscounts the run as unclosed, it appends a spurious
*to the text. And since remend runs on every render whilemode="streaming"— including renders after the stream has completed — the stray*ends up permanently in the final output, not just in a transient mid-stream state.This is especially painful for Korean/Japanese content, where particles/suffixes attach directly to the emphasized word (e.g.
*단어*로), so LLM output hits this pattern constantly. It corrupts the message whenever the number of intraword closers in a block is odd (even counts cancel out in remend's counter, which is why the bug looks intermittent).Steps to Reproduce
<Streamdown>{'*foo*bar'}</Streamdown>(defaults:mode="streaming",parseIncompleteMarkdownon)<Streamdown mode="static">{'*foo*bar'}</Streamdown>*that never goes awayCalling remend directly shows the corruption at the string level:
Expected Behavior
<em>foo</em>bar— identical tomode="static"and to CommonMark reference implementations. Complete, valid markdown should pass through remend unchanged.Actual Behavior
<em>foo</em>bar*— a spurious literal*is appended, and it persists after streaming finishes.More real-world examples (all complete texts, all corrupted the same way):
this is *real*ly goodthis is really good*this is really good이것은 *기울임*으로 표시이것은 기울임으로 표시*이것은 기울임으로 표시Code Sample
Streamdown Version
2.5.0 (remend 1.3.0; also reproduces against current
main)React Version
18.2.0
Node.js Version
24.18.0
Browser(s)
Not applicable (Node.js)
Operating System
macOS
Additional Context
The skip happens in
packages/remend/src/emphasis-handlers.ts— the "Skip if flanked by word chars on both sides" checks treat an intraword*as a non-delimiter. That matches CommonMark's rule for_, but not for*: an intraword*is both left- and right-flanking, so it can open and close emphasis (cf. the CommonMark spec example5*6*78→5<em>6</em>78).So when a run has a valid opener (e.g. after whitespace) and its closer happens to be intraword, remend's open-counter stays at 1 and the completion logic appends
*at the end of the block. A fix could be to count intraword*as a potential closer whenever a run is currently open, mirroring micromark's delimiter matching.Workaround we're using in the meantime:
remend={{ italic: false }}.