Skip to content

remend miscounts intraword italic closers (*foo*bar) and appends a spurious trailing * to complete text #577

Description

@JHSeo-git

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

  1. Render <Streamdown>{'*foo*bar'}</Streamdown> (defaults: mode="streaming", parseIncompleteMarkdown on)
  2. Compare with <Streamdown mode="static">{'*foo*bar'}</Streamdown>
  3. 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*785<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 }}.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions