Skip to content

Commit 104798e

Browse files
committed
Resolves #303
1 parent b8c8c79 commit 104798e

File tree

5 files changed

+172
-20
lines changed

5 files changed

+172
-20
lines changed

.changeset/better-badgers-think.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"streamdown": patch
3+
"remend": patch
4+
---
5+
6+
Make remend configurable

apps/website/content/docs/configuration.mdx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Streamdown can be configured to suit your needs. This guide will walk you throug
1818
type: 'boolean',
1919
default: 'true',
2020
},
21+
remend: {
22+
description: 'Configure which Markdown completions remend should perform',
23+
type: 'RemendOptions',
24+
},
2125
isAnimating: {
2226
description: 'Indicates if content is currently streaming (disables copy buttons)',
2327
type: 'boolean',
@@ -151,3 +155,68 @@ The `controls` prop can be configured granularly:
151155
{markdown}
152156
</Streamdown>
153157
```
158+
159+
### Remend Options
160+
161+
The `remend` prop configures which Markdown completions are performed during streaming. All options default to `true` when not specified. Set an option to `false` to disable that completion:
162+
163+
<TypeTable
164+
type={{
165+
links: {
166+
description: 'Complete incomplete links',
167+
type: 'boolean',
168+
default: 'true',
169+
},
170+
images: {
171+
description: 'Complete incomplete images',
172+
type: 'boolean',
173+
default: 'true',
174+
},
175+
bold: {
176+
description: 'Complete bold formatting (**)',
177+
type: 'boolean',
178+
default: 'true',
179+
},
180+
italic: {
181+
description: 'Complete italic formatting (* and _)',
182+
type: 'boolean',
183+
default: 'true',
184+
},
185+
boldItalic: {
186+
description: 'Complete bold-italic formatting (***)',
187+
type: 'boolean',
188+
default: 'true',
189+
},
190+
inlineCode: {
191+
description: 'Complete inline code formatting (`)',
192+
type: 'boolean',
193+
default: 'true',
194+
},
195+
strikethrough: {
196+
description: 'Complete strikethrough formatting (~~)',
197+
type: 'boolean',
198+
default: 'true',
199+
},
200+
katex: {
201+
description: 'Complete block KaTeX math ($$)',
202+
type: 'boolean',
203+
default: 'true',
204+
},
205+
setextHeadings: {
206+
description: 'Handle incomplete setext headings',
207+
type: 'boolean',
208+
default: 'true',
209+
},
210+
}}
211+
/>
212+
213+
```tsx title="app/page.tsx"
214+
<Streamdown
215+
remend={{
216+
links: false, // Disable link completion
217+
katex: false, // Disable KaTeX completion
218+
}}
219+
>
220+
{markdown}
221+
</Streamdown>
222+
```

packages/remend/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,34 @@ const completed = remend(partialLink);
5454
// Result: "Check out [this link](streamdown:incomplete-link)"
5555
```
5656

57+
### Configuration
58+
59+
You can selectively disable specific completions by passing an options object. All options default to `true`:
60+
61+
```typescript
62+
import remend from "remend";
63+
64+
// Disable link and KaTeX completion
65+
const completed = remend(partialMarkdown, {
66+
links: false,
67+
katex: false,
68+
});
69+
```
70+
71+
Available options:
72+
73+
| Option | Description |
74+
|--------|-------------|
75+
| `links` | Complete incomplete links |
76+
| `images` | Complete incomplete images |
77+
| `bold` | Complete bold formatting (`**`) |
78+
| `italic` | Complete italic formatting (`*` and `_`) |
79+
| `boldItalic` | Complete bold-italic formatting (`***`) |
80+
| `inlineCode` | Complete inline code formatting (`` ` ``) |
81+
| `strikethrough` | Complete strikethrough formatting (`~~`) |
82+
| `katex` | Complete block KaTeX math (`$$`) |
83+
| `setextHeadings` | Handle incomplete setext headings |
84+
5785
### Usage with Remark
5886

5987
Remend is a preprocessor that must be run on the raw Markdown string **before** passing it into the unified/remark processing pipeline:

packages/remend/src/index.ts

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,37 @@ import { handleIncompleteLinksAndImages } from "./link-image-handler";
1111
import { handleIncompleteSetextHeading } from "./setext-heading-handler";
1212
import { handleIncompleteStrikethrough } from "./strikethrough-handler";
1313

14+
/**
15+
* Configuration options for the remend function.
16+
* All options default to `true` when not specified.
17+
* Set an option to `false` to disable that specific completion.
18+
*/
19+
export type RemendOptions = {
20+
/** Complete links and images (e.g., `[text](url` → `[text](streamdown:incomplete-link)`) */
21+
links?: boolean;
22+
/** Complete images (e.g., `![alt](url` → removed) */
23+
images?: boolean;
24+
/** Complete bold formatting (e.g., `**text` → `**text**`) */
25+
bold?: boolean;
26+
/** Complete italic formatting (e.g., `*text` → `*text*` or `_text` → `_text_`) */
27+
italic?: boolean;
28+
/** Complete bold-italic formatting (e.g., `***text` → `***text***`) */
29+
boldItalic?: boolean;
30+
/** Complete inline code formatting (e.g., `` `code `` → `` `code` ``) */
31+
inlineCode?: boolean;
32+
/** Complete strikethrough formatting (e.g., `~~text` → `~~text~~`) */
33+
strikethrough?: boolean;
34+
/** Complete block KaTeX math (e.g., `$$equation` → `$$equation$$`) */
35+
katex?: boolean;
36+
/** Handle incomplete setext headings to prevent misinterpretation */
37+
setextHeadings?: boolean;
38+
};
39+
40+
// Helper to check if an option is enabled (defaults to true)
41+
const isEnabled = (option: boolean | undefined): boolean => option !== false;
42+
1443
// Parses markdown text and removes incomplete tokens to prevent partial rendering
15-
const remend = (text: string): string => {
44+
const remend = (text: string, options?: RemendOptions): string => {
1645
if (!text || typeof text !== "string") {
1746
return text;
1847
}
@@ -23,31 +52,48 @@ const remend = (text: string): string => {
2352

2453
// Handle incomplete setext headings first (before other processing)
2554
// This prevents partial list items (like "-") from being interpreted as heading underlines
26-
result = handleIncompleteSetextHeading(result);
55+
if (isEnabled(options?.setextHeadings)) {
56+
result = handleIncompleteSetextHeading(result);
57+
}
2758

2859
// Handle incomplete links and images
29-
const processedResult = handleIncompleteLinksAndImages(result);
60+
// Note: links and images share the same handler
61+
if (isEnabled(options?.links) || isEnabled(options?.images)) {
62+
const processedResult = handleIncompleteLinksAndImages(result);
3063

31-
// If we added an incomplete link marker, don't process other formatting
32-
// as the content inside the link should be preserved as-is
33-
if (processedResult.endsWith("](streamdown:incomplete-link)")) {
34-
return processedResult;
35-
}
64+
// If we added an incomplete link marker, don't process other formatting
65+
// as the content inside the link should be preserved as-is
66+
if (processedResult.endsWith("](streamdown:incomplete-link)")) {
67+
return processedResult;
68+
}
3669

37-
result = processedResult;
70+
result = processedResult;
71+
}
3872

3973
// Handle various formatting completions
4074
// Handle triple asterisks first (most specific)
41-
result = handleIncompleteBoldItalic(result);
42-
result = handleIncompleteBold(result);
43-
result = handleIncompleteDoubleUnderscoreItalic(result);
44-
result = handleIncompleteSingleAsteriskItalic(result);
45-
result = handleIncompleteSingleUnderscoreItalic(result);
46-
result = handleIncompleteInlineCode(result);
47-
result = handleIncompleteStrikethrough(result);
75+
if (isEnabled(options?.boldItalic)) {
76+
result = handleIncompleteBoldItalic(result);
77+
}
78+
if (isEnabled(options?.bold)) {
79+
result = handleIncompleteBold(result);
80+
}
81+
if (isEnabled(options?.italic)) {
82+
result = handleIncompleteDoubleUnderscoreItalic(result);
83+
result = handleIncompleteSingleAsteriskItalic(result);
84+
result = handleIncompleteSingleUnderscoreItalic(result);
85+
}
86+
if (isEnabled(options?.inlineCode)) {
87+
result = handleIncompleteInlineCode(result);
88+
}
89+
if (isEnabled(options?.strikethrough)) {
90+
result = handleIncompleteStrikethrough(result);
91+
}
4892

4993
// Handle KaTeX formatting (only block math with $$)
50-
result = handleIncompleteBlockKatex(result);
94+
if (isEnabled(options?.katex)) {
95+
result = handleIncompleteBlockKatex(result);
96+
}
5197
// Note: We don't handle inline KaTeX with single $ as they're likely currency symbols
5298

5399
return result;

packages/streamdown/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import remarkCjkFriendly from "remark-cjk-friendly";
1919
import remarkCjkFriendlyGfmStrikethrough from "remark-cjk-friendly-gfm-strikethrough";
2020
import remarkGfm from "remark-gfm";
2121
import remarkMath from "remark-math";
22-
import remend from "remend";
22+
import remend, { type RemendOptions } from "remend";
2323
import type { BundledTheme } from "shiki";
2424
import type { Pluggable } from "unified";
2525
import { components as defaultComponents } from "./lib/components";
@@ -34,6 +34,7 @@ const START_DOLLAR_PATTERN = /^\$[^$]/;
3434
const END_DOLLAR_PATTERN = /[^$]\$$/;
3535

3636
export type { MermaidConfig } from "mermaid";
37+
export type { RemendOptions } from "remend";
3738
export type { BundledLanguageName } from "./lib/code-block/bundled-languages";
3839

3940
// biome-ignore lint/performance/noBarrelFile: "required"
@@ -81,6 +82,7 @@ export type StreamdownProps = Options & {
8182
isAnimating?: boolean;
8283
caret?: keyof typeof carets;
8384
cdnUrl?: string | null;
85+
remend?: RemendOptions;
8486
};
8587

8688
export const defaultRehypePlugins: Record<string, Pluggable> = {
@@ -280,6 +282,7 @@ export const Streamdown = memo(
280282
parseMarkdownIntoBlocksFn = parseMarkdownIntoBlocks,
281283
caret,
282284
cdnUrl,
285+
remend: remendOptions,
283286
...props
284287
}: StreamdownProps) => {
285288
// All hooks must be called before any conditional returns
@@ -294,9 +297,9 @@ export const Streamdown = memo(
294297
return "";
295298
}
296299
return mode === "streaming" && shouldParseIncompleteMarkdown
297-
? remend(children)
300+
? remend(children, remendOptions)
298301
: children;
299-
}, [children, mode, shouldParseIncompleteMarkdown]);
302+
}, [children, mode, shouldParseIncompleteMarkdown, remendOptions]);
300303

301304
const blocks = useMemo(
302305
() => parseMarkdownIntoBlocksFn(processedChildren),

0 commit comments

Comments
 (0)