vercel / vercel/streamdown

CJK autolink boundary handling leaves subsequent URLs as plain text

Open
#623 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
5.6k
Forks
302
Avg merge
3d 19h
Merged PRs (30d)
14

Description

Bug Description

With @streamdown/cjk enabled, multiple bare URLs separated by fullwidth semicolons (;, U+FF1B) without whitespace render with only the first URL clickable. Subsequent URLs remain plain text. This appears to be an uncovered case in #327 (fixing #326), rather than an error in the boundary index calculation.

Steps to Reproduce

Render the code sample below with mode="static", then with mode="streaming". In both cases only the first URL becomes a link. Adding a space after each semicolon, or writing three explicit Markdown links, makes all three links work.

Expected Behavior

All three URLs should be separate clickable links, with the semicolons preserved as plain text.

Actual Behavior

Only https://example.com/a is clickable. The rest is plain text. With a custom a renderer to inspect the HTML:

<p>参考资料:<a href="https://example.com/a">https://example.com/a</a>;https://example.com/b;https://example.com/c</p>
Code Sample
import { Streamdown } from 'streamdown';
import { cjk } from '@streamdown/cjk';

const markdown =
  '参考资料:https://example.com/a;https://example.com/b;https://example.com/c';

export default function App() {
  return (
    <Streamdown mode="static" plugins={{ cjk }}>
      {markdown}
    </Streamdown>
  );
}
Streamdown Version

2.5.0 and 2.6.0; @streamdown/cjk 1.0.3

React Version

19.2.4

Node.js Version

24.13.1

Browser(s)

No response

Operating System

None

Additional Context

The following diagram illustrates the observed parsing result; it is not a browser screenshot.

flowchart TD
  input["Input: URL A;URL B;URL C"] --> gfm["GFM: one link containing A;B;C"]
  gfm --> boundary["CJK boundary: split at the first ;"]
  boundary --> actual["Actual: link A + plain text ;B;C"]
  input -. "Expected rendering" .-> expected["link A + ; + link B + ; + link C"]
  style boundary fill:#fff3cd,stroke:#b58105,color:#222
  style actual fill:#ffe8e8,stroke:#c74343,color:#222
  style expected fill:#e7f6ec,stroke:#348450,color:#222

In #327, remarkCjkAutolinkBoundary finds the first CJK punctuation boundary and replaces the original link with:

const trimmedLink = buildAutolink(trimmedUrl, node);
const trailingText = buildTrailingText(trailing);
parent.children.splice(index, 1, trimmedLink, trailingText);

For URL。谢谢, this correctly preserves trailing prose as text. For URL A;URL B;URL C, the trailing part contains two more URLs. It becomes a text node and is not re-linkified by this link-node visitor.

Without the boundary handling, GFM also does not produce three correct links: it treats the sequence as one long URL. This report is about preserving subsequent URLs while applying the CJK boundary fix, not claiming the pre-#327 output was correct.

Verification:

  • Reproduced with Streamdown 2.5.0 and @streamdown/cjk 1.0.3.
  • Also tested the original three-URL message after upgrading to Streamdown 2.6.0 with @streamdown/cjk 1.0.3, without our local workaround: the same two static/streaming regression tests fail.
  • Adding a space after each semicolon makes all three links render correctly.
  • Writing three explicit Markdown links also works.
  • Tests used React server rendering with complete content in both modes; incremental token delivery was not tested.

Could the CJK autolink handling preserve or re-detect subsequent bare URLs after splitting at punctuation? A regression test for multiple URLs separated by fullwidth semicolons would cover this case.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the remarkCjkAutolinkBoundary entry point described in the issue and inspect the static and streaming regression tests that reproduce the fullwidth-semicolon case. Verify how the trailing text is handled after splitting the first link. Done means all three bare URLs render as separate clickable links with semicolons preserved in both modes.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend, testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.