






















Learn how Streamdown handles incomplete Markdown syntax during AI streaming with remend.
One of Streamdown's most powerful features is its ability to intelligently parse and style incomplete Markdown blocks using the remend package. This feature, called unterminated block parsing, ensures that your streaming content looks polished even before the AI finishes its response.
Remend is a lightweight, standalone preprocessor that completes incomplete Markdown syntax. Streamdown integrates remend by default, but you can also use it independently in your own projects. See the remend documentation for standalone usage.
When AI models stream Markdown content token-by-token, the content often arrives incomplete:
Without proper handling, this would either:
Streamdown solves this by detecting incomplete patterns and intelligently completing them for rendering purposes.
Remend (Streamdown's preprocessing layer) analyzes the incoming Markdown and identifies common patterns that might be incomplete. When detected, it automatically adds the closing syntax so the content renders correctly, then seamlessly updates when the actual closing syntax arrives.
The preprocessing happens before the markdown is passed into the unified/remark pipeline, operating on the raw string level for maximum performance.
**text)**This is bold text that hasn't been closed yetThe parser detects the opening ** and adds a closing ** to ensure the text renders as bold.
*text or _text)*This is italic text
_This is also italicSingle asterisks or underscores are completed automatically.
***text)***This is bold and italicTriple asterisks for combined formatting are handled.
`code)Incomplete inline code blocks are closed with a backtick.
~~text)~~This text is being crossed outStrikethrough formatting is completed automatically.
[text](url))Streamdown handles several link scenarios:
Incomplete link text:
Completes to: [Click here](streamdown:incomplete-link)
Incomplete URL:
[Click here](https://examplCompletes to: [Click here](streamdown:incomplete-link)
The special streamdown:incomplete-link URL ensures the link renders visually but doesn't navigate anywhere.
Text-only mode:
If you prefer to display just the link text without any link markup during streaming (for better compatibility with other markdown renderers like react-markdown), use the linkMode option:
<Streamdown remend={{ linkMode: 'text-only' }}>
{markdown}
</Streamdown>With linkMode: 'text-only':
[Click here → Click here (plain text)[Click here](https://exampl → Click here (plain text)The link renders as a proper link once complete.
Custom component override:
For full control over how incomplete links render, you can override the a component. This approach lets you customize styling, add loading indicators, or implement custom behavior:
<Streamdown
components={{
a: ({ href, children, ...props }) => {
const isIncomplete = href === 'streamdown:incomplete-link';
if (isIncomplete) {
// Render as plain text with custom styling
return <span className="text-muted-foreground">{children}</span>;
}
return (
<a href={href} target="_blank" rel="noreferrer" {...props}>
{children}
</a>
);
},
}}
>
{markdown}
</Streamdown>For react-markdown users consuming the remend package directly:
import ReactMarkdown from 'react-markdown';
import remend from 'remend';
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => {
if (href === 'streamdown:incomplete-link') {
return <>{children}</>;
}
return <a href={href} {...props}>{children}</a>;
},
}}
>
{remend(streamingText)}
</ReactMarkdown>For incomplete images, Streamdown removes them entirely rather than showing broken image placeholders:
;
// Result: "[Click here](streamdown:incomplete-link)"
// Text-only mode: display plain text
remend("[Click here](http://exampl", { linkMode: 'text-only' });
// Result: "Click here"See the remend package for more details on standalone usage.
You can extend remend with custom handlers to complete your own markers during streaming. This is useful for domain-specific syntax like custom tags or markers that your AI might output.
import remend, { type RemendHandler } from 'remend';
const jokeHandler: RemendHandler = {
name: 'joke',
handle: (text) => {
// Complete <<<JOKE>>> marks that aren't closed
const match = text.match(/<<<JOKE>>>([^<]*)$/);
if (match && !text.endsWith('<<</JOKE>>>')) {
return `${text}<<</JOKE>>>`;
}
return text;
},
priority: 80, // Runs after most built-ins (-10 to 70)
};
const result = remend(content, { handlers: [jokeHandler] });Each handler has three properties:
name - Unique identifier for the handlerhandle - Transform function that receives text and returns modified textpriority - Optional execution order (lower runs first, default: 100)Built-in handlers use priorities -10 to 70:
| Handler | Priority |
|---|---|
comparisonOperators | -10 |
htmlTags | -5 |
setextHeadings | 0 |
links | 10 |
boldItalic | 20 |
bold | 30 |
italic | 40-42 |
inlineCode | 50 |
strikethrough | 60 |
katex | 70 |
Custom handlers default to priority 100, running after all built-ins. Set a lower priority to run before specific built-ins.
Example: Running Before Bold Handling
To process content before bold formatting is completed (priority 30), set a lower priority:
const preprocessHandler: RemendHandler = {
name: 'preprocess',
handle: (text) => {
// Transform content before bold is completed
return text.replace(/\{\{highlight\}\}/g, '**');
},
priority: 25, // Runs before bold (30) and after links (10)
};Example: Running After All Built-ins
For post-processing, use the default priority or higher:
const postprocessHandler: RemendHandler = {
name: 'postprocess',
handle: (text) => {
// Clean up after all built-in handlers
return text.trim();
},
priority: 150, // Runs after all built-ins
};Remend exports utilities to help custom handlers detect context:
import {
isWithinCodeBlock,
isWithinMathBlock,
isWithinLinkOrImageUrl,
isWordChar,
} from 'remend';
const handler: RemendHandler = {
name: 'custom',
handle: (text) => {
// Skip processing inside code blocks
if (isWithinCodeBlock(text, text.length - 1)) {
return text;
}
// Your completion logic here
return text;
},
};Remend includes intelligent rules to avoid false positives:
The parser won't close formatting markers that appear at the start of list items:
- **
- Item with bold marker onlyThis prevents prematurely closing bold formatting that might be intentional list structure.
Formatting within complete code blocks is left untouched:
```python
def foo():
# This **won't** be treated as incomplete bold
return "bar"
```Underscores within math blocks are not treated as italic markers:
$$
E = m \times c^2
x_i = y_j
$$Asterisks and underscores within words (like variable names or between alphanumeric characters) are preserved:
const user_name = "john_doe";
234234*123
hello*worldThis ensures that text like product codes or mathematical expressions with asterisks are not mistakenly interpreted as italic formatting.
Remend is highly optimized for streaming scenarios:
As content streams in:
**This → Renders nothing (too short)**This is bol → Renders as This is bol**This is bold** → Renders as This is boldWith default linkMode: 'protocol':
[Cli → Renders nothing[Click here → Renders as Click here[Click here](https:// → Renders as Click here[Click here](https://example.com) → Renders as Click hereWith linkMode: 'text-only':
[Cli → Renders nothing[Click here → Renders as plain text: Click here[Click here](https:// → Renders as plain text: Click here[Click here](https://example.com) → Renders as Click here`const → Renders as const`const foo = → Renders as const foo =`const foo = "bar"` → Renders as const foo = "bar"此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。