svelte-streamdown 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -47
- package/dist/Block.svelte +19 -8
- package/dist/Block.svelte.d.ts +2 -0
- package/dist/Elements/Code.svelte +15 -4
- package/dist/Elements/Code.svelte.d.ts +2 -0
- package/dist/Elements/Element.svelte +32 -11
- package/dist/Elements/Element.svelte.d.ts +1 -0
- package/dist/Elements/Mermaid.svelte +5 -11
- package/dist/Elements/Mermaid.svelte.d.ts +1 -0
- package/dist/Elements/TableDownload.svelte +44 -3
- package/dist/Elements/TableDownload.svelte.d.ts +3 -1
- package/dist/Elements/fallbacks/CodeFallback.svelte +16 -6
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +3 -2
- package/dist/Elements/icons.d.ts +1 -0
- package/dist/Elements/icons.js +8 -0
- package/dist/Streamdown.svelte +68 -6
- package/dist/context.svelte.d.ts +37 -1
- package/dist/context.svelte.js +3 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/marked/index.d.ts +11 -4
- package/dist/marked/index.js +54 -45
- package/dist/marked/marked-mdx.d.ts +27 -1
- package/dist/marked/marked-mdx.js +37 -8
- package/dist/theme.d.ts +6 -0
- package/dist/theme.js +12 -4
- package/dist/utils/expand.svelte.d.ts +21 -0
- package/dist/utils/expand.svelte.js +46 -0
- package/dist/utils/fence.d.ts +12 -1
- package/dist/utils/fence.js +34 -17
- package/dist/utils/line-numbers.d.ts +23 -0
- package/dist/utils/line-numbers.js +30 -0
- package/dist/utils/normalize-html-indentation.d.ts +10 -0
- package/dist/utils/normalize-html-indentation.js +52 -0
- package/dist/utils/parse-incomplete-markdown.d.ts +20 -6
- package/dist/utils/parse-incomplete-markdown.js +173 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,12 +77,63 @@ Full support for
|
|
|
77
77
|
> [!NOTE]
|
|
78
78
|
> 🧠 **AI Prompting Tip:** For best results, use our [comprehensive prompt](/prompting) covering all supported markdown features.
|
|
79
79
|
|
|
80
|
+
### 🏷️ Raw HTML
|
|
81
|
+
|
|
82
|
+
`renderHtml` decides what happens to HTML in the markdown: off (the default) shows the source as
|
|
83
|
+
literal text, `true` renders it, and a function lets you sanitize it yourself and return the string.
|
|
84
|
+
|
|
85
|
+
Pretty-printed HTML has one markdown trap: four leading spaces after a blank line is an indented
|
|
86
|
+
code block, tag or not, so a nested `<div>` shows up in a code box instead of rendering.
|
|
87
|
+
`normalizeHtmlIndentation` dedents tag lines before parsing and leaves `<pre>` and `<code>` bodies
|
|
88
|
+
byte-for-byte:
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<Streamdown {content} renderHtml normalizeHtmlIndentation />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The trap only bites where blocks are not trimmed: `static`, `parseIncompleteMarkdown={false}`,
|
|
95
|
+
or calling the exported function on raw text. The default streaming path trims each block, which
|
|
96
|
+
strips the leading four spaces, so it never sees the code box in the first place.
|
|
97
|
+
|
|
98
|
+
It is off by default because dedenting is lossy, and it is exported as a plain function
|
|
99
|
+
(`import { normalizeHtmlIndentation } from 'svelte-streamdown'`) if you would rather pre-process
|
|
100
|
+
the string yourself.
|
|
101
|
+
|
|
80
102
|
### 💻 Interactive Code Blocks
|
|
81
103
|
|
|
82
104
|
- Syntax highlighting powered by [@tanstack/highlight](https://github.com/TanStack/highlight) (synchronous, SSR-friendly, ~31KB min / ~11KB gzip for every language)
|
|
83
105
|
- Copy-to-clipboard functionality
|
|
84
106
|
- Download the snippet with the extension of its language
|
|
85
107
|
- Support any `@tanstack/highlight` theme, or your own
|
|
108
|
+
- Optional line numbers (`lineNumbers`, off by default)
|
|
109
|
+
|
|
110
|
+
#### Line numbers
|
|
111
|
+
|
|
112
|
+
`lineNumbers` numbers every code block. It is a CSS counter on the line that is already
|
|
113
|
+
rendered — no extra element per line, and the numbers are pseudo-content, so copy,
|
|
114
|
+
download and text selection only ever give you the code.
|
|
115
|
+
|
|
116
|
+
```svelte
|
|
117
|
+
<Streamdown {content} lineNumbers />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Three words in the fence's info string, after the language, override the prop for
|
|
121
|
+
that block: `lineNumbers` numbers it even when the prop is off, `noLineNumbers` leaves
|
|
122
|
+
it unnumbered even when the prop is on, and `startLine=N` starts the count at N instead
|
|
123
|
+
of 1 (anything non-numeric is ignored).
|
|
124
|
+
|
|
125
|
+
````markdown
|
|
126
|
+
```ts startLine=10
|
|
127
|
+
const a = 1;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts noLineNumbers
|
|
131
|
+
const b = 2;
|
|
132
|
+
```
|
|
133
|
+
````
|
|
134
|
+
|
|
135
|
+
The gutter's width and colour are theme data (`theme.code.lineNumber`), so they follow
|
|
136
|
+
your theme like every other class.
|
|
86
137
|
|
|
87
138
|
### 🔢 Mathematical Expressions
|
|
88
139
|
|
|
@@ -157,6 +208,18 @@ pie title Project Time Allocation
|
|
|
157
208
|
Tables copy and download as Markdown, HTML, CSV or TSV — see [Controls](#-controls) for the
|
|
158
209
|
separator and filename options, and for the exported table utilities.
|
|
159
210
|
|
|
211
|
+
#### Fullscreen
|
|
212
|
+
|
|
213
|
+
Wide tables get an expand toggle next to copy and download: it lifts the table out of the flow
|
|
214
|
+
(`position: fixed`, the whole viewport) so all of its columns are reachable, and the toolbar is
|
|
215
|
+
repositioned to the top right so copy and download stay usable. `Escape` or the same button — now a
|
|
216
|
+
close icon — collapses it and puts focus back on the toggle. While expanded the wrapper is a
|
|
217
|
+
`role="dialog"` container labelled with `translations.controls.table`, and it carries
|
|
218
|
+
`data-expanded="true"` plus the `theme.table.expanded` classes. It is deliberately not
|
|
219
|
+
`aria-modal`: the toolbar is a sibling of the wrapper, not a child, so marking the wrapper modal
|
|
220
|
+
would hide those buttons from assistive tech — that comes with a focus trap in a later release.
|
|
221
|
+
Turn it off with `controls={{ table: { fullscreen: false } }}`.
|
|
222
|
+
|
|
160
223
|
#### Colspan
|
|
161
224
|
|
|
162
225
|
| H1 | H2 | H3 |
|
|
@@ -467,6 +530,9 @@ The animation system works by:
|
|
|
467
530
|
2. **Sequential Animation**: Each token animates as it is received
|
|
468
531
|
3. **Block-level Animation**: Entire blocks (paragraphs, headings, code blocks) animate as units
|
|
469
532
|
|
|
533
|
+
> [!NOTE]
|
|
534
|
+
> Only text that arrives in **streamed-sized appends** to `content` is animated. A bulk update — `content` replaced by a different document, a jump back to an earlier prefix, or a single append of more than ~2 KB such as pasting a whole answer or a "show all" — renders without animation, and the next streamed append animates again. Animating a whole document at once would start thousands of CSS animations in a single frame and stall the page.
|
|
535
|
+
|
|
470
536
|
### Animation Types
|
|
471
537
|
|
|
472
538
|
Choose from 4 distinct animation styles:
|
|
@@ -750,44 +816,48 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
750
816
|
|
|
751
817
|
## 📋 Props API
|
|
752
818
|
|
|
753
|
-
| Prop | Type | Default | Description
|
|
754
|
-
| -------------------------- | -------------------------------------------------------------------------------- | ---------------------- |
|
|
755
|
-
| `content` | `string` | - | **Required.** The markdown content to render
|
|
756
|
-
| `sources` | `Record<string, any>` | - | Citation data object for inline citations
|
|
757
|
-
| `class` | `string` | - | CSS class names for the wrapper element
|
|
758
|
-
| `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax
|
|
759
|
-
| `defaultOrigin` | `string` | - | Default origin for relative URLs
|
|
760
|
-
| `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links
|
|
761
|
-
| `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images
|
|
762
|
-
| `renderHtml` | `boolean \| ((token) => string)` | `false` | Render raw HTML blocks and inline tags. When off, the HTML source is shown as literal text instead of being dropped. Pass a function to sanitize and return the HTML string yourself.
|
|
763
|
-
| `inlineCitationsMode` | `'list' \| 'carousel'` | `'carousel'` | How an inline citation popover presents its sources
|
|
764
|
-
| `translations` | `{ alert?: {...}, controls?: {...} }` | `defaultTranslations` | Override the built-in alert titles and control labels — see [Translations](#-translations)
|
|
765
|
-
| `icons` | `Partial<Record<IconName, Snippet>>` | - | Replace any built-in icon (`copy`, `check`, `download`, `fullscreen`, `zoomIn`, `zoomOut`, `fitView`, `chevronLeft`, `chevronRight`, `note`, `tip`, `warning`, `caution`, `important`) with your own snippet
|
|
766
|
-
| `static` | `boolean` | `false` | Render finished content: skips the incomplete-markdown pass and the streaming animation
|
|
767
|
-
| `element` | `HTMLElement` | - | `bind:element` to get the wrapper node
|
|
768
|
-
| `streamdown` | `StreamdownContext` | - | `bind:streamdown` to read the resolved context (theme, controls, footnotes, sources)
|
|
769
|
-
| `theme` | `DeepPartial<Theme>` | - | Custom theme overrides
|
|
770
|
-
| `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides
|
|
771
|
-
| `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme
|
|
772
|
-
| `highlightTheme` | `string` | auto (dark-mode aware) | Code highlighting theme. Defaults to `github-dark` in dark mode / `github-light` otherwise. Any other value must be a key registered via `highlightThemes`. See [Highlight themes](#highlight-themes).
|
|
773
|
-
| `highlightThemes` | `Record<string, HighlightTheme>` | - | Register additional pre-imported themes (e.g. `{ dracula }`) so they can be selected via `highlightTheme`, including dynamic light/dark switching.
|
|
774
|
-
| `highlightLanguages` | `LanguageDefinition[]` | - | Additional languages built with `defineLanguage` (merged with the 30 built-in ones)
|
|
775
|
-
| `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration
|
|
776
|
-
| `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options
|
|
777
|
-
| `animation` | `AnimationConfig` | - | Animation configuration for streaming content
|
|
778
|
-
| `animation.enabled` | `boolean` | `false` | Enable/disable animations
|
|
779
|
-
| `animation.type` | `'fade' \| 'blur' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance
|
|
780
|
-
| `animation.duration` | `number` | `500` | Animation duration in milliseconds
|
|
781
|
-
| `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations
|
|
782
|
-
| `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations
|
|
783
|
-
| `animation.animateOnMount` | `boolean` | `false` | Run the token animation on mount or not, useful if you render the Streamdown component in the same time as the first token is receive from the LLM
|
|
784
|
-
| `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens
|
|
785
|
-
| `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`)
|
|
786
|
-
| `
|
|
787
|
-
| `
|
|
788
|
-
| `
|
|
789
|
-
| `
|
|
790
|
-
| `
|
|
819
|
+
| Prop | Type | Default | Description |
|
|
820
|
+
| -------------------------- | -------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
821
|
+
| `content` | `string` | - | **Required.** The markdown content to render |
|
|
822
|
+
| `sources` | `Record<string, any>` | - | Citation data object for inline citations |
|
|
823
|
+
| `class` | `string` | - | CSS class names for the wrapper element |
|
|
824
|
+
| `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax |
|
|
825
|
+
| `defaultOrigin` | `string` | - | Default origin for relative URLs |
|
|
826
|
+
| `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links |
|
|
827
|
+
| `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images |
|
|
828
|
+
| `renderHtml` | `boolean \| ((token) => string)` | `false` | Render raw HTML blocks and inline tags. When off, the HTML source is shown as literal text instead of being dropped. Pass a function to sanitize and return the HTML string yourself. |
|
|
829
|
+
| `inlineCitationsMode` | `'list' \| 'carousel'` | `'carousel'` | How an inline citation popover presents its sources |
|
|
830
|
+
| `translations` | `{ alert?: {...}, controls?: {...} }` | `defaultTranslations` | Override the built-in alert titles and control labels — see [Translations](#-translations) |
|
|
831
|
+
| `icons` | `Partial<Record<IconName, Snippet>>` | - | Replace any built-in icon (`copy`, `check`, `download`, `fullscreen`, `close`, `zoomIn`, `zoomOut`, `fitView`, `chevronLeft`, `chevronRight`, `note`, `tip`, `warning`, `caution`, `important`) with your own snippet |
|
|
832
|
+
| `static` | `boolean` | `false` | Render finished content: skips the incomplete-markdown pass and the streaming animation |
|
|
833
|
+
| `element` | `HTMLElement` | - | `bind:element` to get the wrapper node |
|
|
834
|
+
| `streamdown` | `StreamdownContext` | - | `bind:streamdown` to read the resolved context (theme, controls, footnotes, sources) |
|
|
835
|
+
| `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
|
|
836
|
+
| `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
|
|
837
|
+
| `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
|
|
838
|
+
| `highlightTheme` | `string` | auto (dark-mode aware) | Code highlighting theme. Defaults to `github-dark` in dark mode / `github-light` otherwise. Any other value must be a key registered via `highlightThemes`. See [Highlight themes](#highlight-themes). |
|
|
839
|
+
| `highlightThemes` | `Record<string, HighlightTheme>` | - | Register additional pre-imported themes (e.g. `{ dracula }`) so they can be selected via `highlightTheme`, including dynamic light/dark switching. |
|
|
840
|
+
| `highlightLanguages` | `LanguageDefinition[]` | - | Additional languages built with `defineLanguage` (merged with the 30 built-in ones) |
|
|
841
|
+
| `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
|
|
842
|
+
| `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
|
|
843
|
+
| `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
|
|
844
|
+
| `animation.enabled` | `boolean` | `false` | Enable/disable animations |
|
|
845
|
+
| `animation.type` | `'fade' \| 'blur' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance |
|
|
846
|
+
| `animation.duration` | `number` | `500` | Animation duration in milliseconds |
|
|
847
|
+
| `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations |
|
|
848
|
+
| `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
|
|
849
|
+
| `animation.animateOnMount` | `boolean` | `false` | Run the token animation on mount or not, useful if you render the Streamdown component in the same time as the first token is receive from the LLM |
|
|
850
|
+
| `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
|
|
851
|
+
| `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`) |
|
|
852
|
+
| `customTags` | `string[]` | `[]` | Extra tag names the MDX tokenizer accepts on top of PascalCase, so `<ai-thinking>` becomes a component instead of a raw HTML block. Keys of `mdxComponents` are allowed automatically — see [Component Naming](#component-naming) |
|
|
853
|
+
| `literalTagContent` | `string[]` | `[]` | Tags whose children render verbatim: no markdown parsing, `**` and `_` left exactly as written. Listing a tag here also allows it, like `customTags` |
|
|
854
|
+
| `normalizeHtmlIndentation` | `boolean` | `false` | Dedent pretty-printed HTML before parsing, so a nested tag indented four spaces after a blank line is not read as an indented code block. `<pre>` and `<code>` bodies are never touched |
|
|
855
|
+
| `components` | `{ code?, mermaid?, math? }` | - | Optional heavy components for syntax highlighting, diagrams, and math rendering |
|
|
856
|
+
| `controls` | `boolean \| { code?, table?, mermaid? }` | all `true` | Toggle and configure the action toolbars for code blocks, tables and mermaid diagrams — see [Controls](#-controls) |
|
|
857
|
+
| `lineNumbers` | `boolean` | `false` | Number the lines of every code block. A fence can override it with a `lineNumbers` / `noLineNumbers` meta word, and start the count at N with `startLine=N`. |
|
|
858
|
+
| `codeBlockMaxHeight` | `string` | - | CSS length that caps the height of code blocks (e.g. `'24rem'`). While content streams in, the block stays scrolled to the bottom unless the reader has scrolled up. |
|
|
859
|
+
| `tableMaxHeight` | `string` | - | CSS length that caps the height of tables, with the same streaming auto-scroll as `codeBlockMaxHeight`. |
|
|
860
|
+
| `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
|
|
791
861
|
|
|
792
862
|
#### All Available Customizable Elements:
|
|
793
863
|
|
|
@@ -829,10 +899,10 @@ Every string the components render themselves — alert titles, button labels, d
|
|
|
829
899
|
/>
|
|
830
900
|
```
|
|
831
901
|
|
|
832
|
-
| Namespace | Keys
|
|
833
|
-
| ---------- |
|
|
834
|
-
| `alert` | `note`, `tip`, `warning`, `caution`, `important`
|
|
835
|
-
| `controls` | `copyCode`, `copiedCode`, `downloadCode`, `copyTable`, `copiedTable`, `downloadTable`, `tableFormatMarkdown`, `tableFormatHtml`, `tableFormatCsv`, `tableFormatTsv`, `downloadDiagram`, `downloadDiagramPng`, `downloadDiagramSvg`, `downloadDiagramMmd`, `zoomIn`, `zoomOut`, `resetView`, `fullscreen`, `exitFullscreen`, `diagram`, `previousCitation`, `nextCitation`, `blockedUrl`, `imageBlocked`, `imageNoDescription`, `linkBlocked` |
|
|
902
|
+
| Namespace | Keys |
|
|
903
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
904
|
+
| `alert` | `note`, `tip`, `warning`, `caution`, `important` |
|
|
905
|
+
| `controls` | `copyCode`, `copiedCode`, `downloadCode`, `copyTable`, `copiedTable`, `downloadTable`, `tableFormatMarkdown`, `tableFormatHtml`, `tableFormatCsv`, `tableFormatTsv`, `tableFullscreen`, `exitTableFullscreen`, `table`, `downloadDiagram`, `downloadDiagramPng`, `downloadDiagramSvg`, `downloadDiagramMmd`, `zoomIn`, `zoomOut`, `resetView`, `fullscreen`, `exitFullscreen`, `diagram`, `previousCitation`, `nextCitation`, `blockedUrl`, `imageBlocked`, `imageNoDescription`, `linkBlocked` |
|
|
836
906
|
|
|
837
907
|
The alert defaults are lowercase because the theme capitalizes them with CSS; if you drop that class, capitalize them here instead.
|
|
838
908
|
|
|
@@ -861,6 +931,7 @@ type Controls =
|
|
|
861
931
|
enabled?: boolean;
|
|
862
932
|
copy?: boolean;
|
|
863
933
|
download?: boolean | { filename?: string | ((token: TableToken) => string) };
|
|
934
|
+
fullscreen?: boolean;
|
|
864
935
|
csvSeparator?: ',' | ';' | '\t' | 'auto';
|
|
865
936
|
};
|
|
866
937
|
mermaid?:
|
|
@@ -892,6 +963,9 @@ type Controls =
|
|
|
892
963
|
text opens correctly in Excel.
|
|
893
964
|
- `mouseWheelZoom` is a gesture rather than a button: it stays on unless you set it to `false` or
|
|
894
965
|
turn every control off with `controls={false}`.
|
|
966
|
+
- `table.fullscreen` is the expand toggle in the table toolbar (on by default). It is a third
|
|
967
|
+
action, so the toolbar survives `copy: false, download: false`; set all three to `false` to get
|
|
968
|
+
rid of it.
|
|
895
969
|
|
|
896
970
|
### Table export utilities
|
|
897
971
|
|
|
@@ -1001,13 +1075,13 @@ Each component supports multiple themeable parts:
|
|
|
1001
1075
|
|
|
1002
1076
|
**Links (`a`)**: `base`, `blocked` (for blocked/unsafe links)
|
|
1003
1077
|
|
|
1004
|
-
**Code (`code`)**: `base`, `container`, `header`, `buttons`, `language`, `line`, `pre`
|
|
1078
|
+
**Code (`code`)**: `base`, `container`, `header`, `buttons`, `language`, `line`, `lineNumber`, `pre`
|
|
1005
1079
|
|
|
1006
1080
|
**Inline Code (`inlineCode`)**: `base`
|
|
1007
1081
|
|
|
1008
1082
|
**Images (`img`)**: `container`, `base`, `downloadButton`
|
|
1009
1083
|
|
|
1010
|
-
**Tables (`table`, `thead`, `tbody`, `tr`, `th`, `td`)**: `base`, `
|
|
1084
|
+
**Tables (`table`, `thead`, `tbody`, `tr`, `th`, `td`)**: `base`, plus `table` and `expanded` (table only)
|
|
1011
1085
|
|
|
1012
1086
|
**Blockquotes (`blockquote`)**: `base`
|
|
1013
1087
|
|
|
@@ -1147,9 +1221,36 @@ MDX components support three attribute value types:
|
|
|
1147
1221
|
|
|
1148
1222
|
### Component Naming
|
|
1149
1223
|
|
|
1150
|
-
-
|
|
1151
|
-
-
|
|
1152
|
-
-
|
|
1224
|
+
- PascalCase names are **always** components: `<Card />`, `<MyComponent />`, `<Component123 />`
|
|
1225
|
+
- Lowercase and hyphenated names are **opt-in**. `<card />`, `<ai-thinking>` and `<mention>` are plain HTML unless you list them, so nothing you write today changes meaning.
|
|
1226
|
+
- A name is listed by putting it in `customTags`, or simply by registering it in `mdxComponents` — its keys are allowlisted for you.
|
|
1227
|
+
|
|
1228
|
+
```svelte
|
|
1229
|
+
<Streamdown
|
|
1230
|
+
content={`<ai-thinking>\nLet me **check** that.\n</ai-thinking>`}
|
|
1231
|
+
customTags={['ai-thinking']}
|
|
1232
|
+
>
|
|
1233
|
+
{#snippet mdx({ token, children })}
|
|
1234
|
+
{#if token.tagName === 'ai-thinking'}
|
|
1235
|
+
<aside class="text-sm text-gray-500">{@render children()}</aside>
|
|
1236
|
+
{/if}
|
|
1237
|
+
{/snippet}
|
|
1238
|
+
</Streamdown>
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
The allowlist is compiled once and shared by the tokenizer and the streaming completer, so a half-typed `<ai-think` is hidden while it streams rather than flashing as text.
|
|
1242
|
+
|
|
1243
|
+
### Literal Tag Content
|
|
1244
|
+
|
|
1245
|
+
Some tags carry data, not markdown. List them in `literalTagContent` and their children become a single text token — underscores, asterisks and backticks survive untouched:
|
|
1246
|
+
|
|
1247
|
+
```svelte
|
|
1248
|
+
<!-- Markdown: <mention user_id="1">@_john_doe_</mention> -->
|
|
1249
|
+
<Streamdown {content} literalTagContent={['mention']} />
|
|
1250
|
+
<!-- renders @_john_doe_, not @john_doe with an italic run -->
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
Attribute names may contain hyphens (`<mention data-id="7">`), and values use the same `attr="string"` / `attr={expression}` forms as PascalCase components.
|
|
1153
1254
|
|
|
1154
1255
|
### Streaming Safety
|
|
1155
1256
|
|
|
@@ -1158,6 +1259,7 @@ MDX components are streaming-safe. Incomplete components are automatically handl
|
|
|
1158
1259
|
- Incomplete tags like `<Component attr` not rendered to prevent runtime errors
|
|
1159
1260
|
- Unclosed components like `<Card>content` are auto-closed with `</Card>`
|
|
1160
1261
|
- Malformed attributes are escaped to prevent rendering errors
|
|
1262
|
+
- Half-typed **HTML** tags are hidden too: `Hello <div cla` renders as `Hello` until the `>` arrives. This only ever applies to the very end of the block still streaming, and only to a name that could still grow into a common HTML element, a PascalCase component or a `customTags` entry, followed by nothing but attributes. A paragraph reading `if a <b then c` keeps its text wherever it sits, and so do `Use the <div element to wrap it.` and `3 < 5`.
|
|
1161
1263
|
|
|
1162
1264
|
This ensures your UI remains stable even when receiving partial markdown from streaming AI responses.
|
|
1163
1265
|
|
package/dist/Block.svelte
CHANGED
|
@@ -4,26 +4,37 @@
|
|
|
4
4
|
import { lex, type StreamdownToken } from './marked/index.js';
|
|
5
5
|
import AnimatedText from './AnimatedText.svelte';
|
|
6
6
|
import { useStreamdown } from './context.svelte.js';
|
|
7
|
-
import { getContext } from 'svelte';
|
|
7
|
+
import { getContext, untrack } from 'svelte';
|
|
8
8
|
|
|
9
9
|
let {
|
|
10
10
|
block,
|
|
11
11
|
static: isStatic = false,
|
|
12
|
-
incomplete = false
|
|
12
|
+
incomplete = false,
|
|
13
|
+
live = false
|
|
13
14
|
}: {
|
|
14
15
|
block: string;
|
|
15
16
|
static?: boolean;
|
|
16
17
|
/** This block ends inside an unfinished code fence (streaming only). */
|
|
17
18
|
incomplete?: boolean;
|
|
19
|
+
/** This is the block still being streamed into — the only one whose tail may be cut. */
|
|
20
|
+
live?: boolean;
|
|
18
21
|
} = $props();
|
|
19
22
|
|
|
20
23
|
const streamdown = useStreamdown();
|
|
21
24
|
// The old code never consulted `streamdown.parseIncompleteMarkdown`; the import
|
|
22
25
|
// is aliased so the context flag and the helper cannot be confused.
|
|
23
26
|
const complete = $derived(!isStatic && streamdown.parseIncompleteMarkdown !== false);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const view = $derived.by(() => {
|
|
28
|
+
const tokens = lex(
|
|
29
|
+
complete ? completeMarkdown(block.trim(), { tags: streamdown.tags, live }) : block,
|
|
30
|
+
streamdown.extensions,
|
|
31
|
+
streamdown.tags
|
|
32
|
+
);
|
|
33
|
+
// Decided when this block's text changes and deliberately not tracked: a
|
|
34
|
+
// bulk update renders plain, and the next streamed chunk must not
|
|
35
|
+
// retroactively animate the blocks it left untouched.
|
|
36
|
+
return { tokens, animate: untrack(() => streamdown.animateUpdate) };
|
|
37
|
+
});
|
|
27
38
|
const insidePopover = getContext('POPOVER');
|
|
28
39
|
</script>
|
|
29
40
|
|
|
@@ -32,9 +43,9 @@
|
|
|
32
43
|
{#if token}
|
|
33
44
|
{@const children = (token as any)?.tokens || []}
|
|
34
45
|
{@const isTextOnlyNode = children.length === 0}
|
|
35
|
-
<Element {token} {incomplete}>
|
|
46
|
+
<Element {token} {incomplete} animate={view.animate}>
|
|
36
47
|
{#if isTextOnlyNode}
|
|
37
|
-
{#if streamdown.animation.enabled && !insidePopover && !isStatic}
|
|
48
|
+
{#if streamdown.animation.enabled && view.animate && !insidePopover && !isStatic}
|
|
38
49
|
<AnimatedText text={'text' in token ? token.text || '' : ''} />
|
|
39
50
|
{:else}
|
|
40
51
|
{'text' in token ? token.text : ''}
|
|
@@ -47,4 +58,4 @@
|
|
|
47
58
|
{/each}
|
|
48
59
|
{/snippet}
|
|
49
60
|
|
|
50
|
-
{@render renderChildren(tokens)}
|
|
61
|
+
{@render renderChildren(view.tokens)}
|
package/dist/Block.svelte.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ type $$ComponentProps = {
|
|
|
3
3
|
static?: boolean;
|
|
4
4
|
/** This block ends inside an unfinished code fence (streaming only). */
|
|
5
5
|
incomplete?: boolean;
|
|
6
|
+
/** This is the block still being streamed into — the only one whose tail may be cut. */
|
|
7
|
+
live?: boolean;
|
|
6
8
|
};
|
|
7
9
|
declare const Block: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
10
|
type Block = ReturnType<typeof Block>;
|
|
@@ -5,18 +5,22 @@
|
|
|
5
5
|
import { highlightLines, languageExtensionMap } from '../utils/hightlighter.svelte.js';
|
|
6
6
|
import type { CodeToken } from '../marked/index.js';
|
|
7
7
|
import { usePinnedScroll } from '../utils/usePinnedScroll.svelte.js';
|
|
8
|
+
import { resolveLineNumbers } from '../utils/line-numbers.js';
|
|
8
9
|
import { checkIcon, copyIcon, downloadIcon } from './icons.js';
|
|
9
10
|
import { srOnly } from './srOnly.js';
|
|
10
11
|
|
|
11
12
|
const {
|
|
12
13
|
token,
|
|
13
14
|
id,
|
|
14
|
-
incomplete = false
|
|
15
|
+
incomplete = false,
|
|
16
|
+
animate = true
|
|
15
17
|
}: {
|
|
16
18
|
token: CodeToken;
|
|
17
19
|
id: string;
|
|
18
20
|
/** The fence is still being streamed; nothing below it is final yet. */
|
|
19
21
|
incomplete?: boolean;
|
|
22
|
+
/** False for the render of a bulk update — a replacement or a paste-sized append (see Block). */
|
|
23
|
+
animate?: boolean;
|
|
20
24
|
} = $props();
|
|
21
25
|
|
|
22
26
|
const streamdown = useStreamdown();
|
|
@@ -51,6 +55,8 @@
|
|
|
51
55
|
|
|
52
56
|
const lines = $derived(highlightLines(code, token.lang, streamdown.highlightLanguages));
|
|
53
57
|
|
|
58
|
+
const lineNumbers = $derived(resolveLineNumbers(token.meta, streamdown.lineNumbers ?? false));
|
|
59
|
+
|
|
54
60
|
// `pre` is already the horizontal scroll container, so capping it there keeps
|
|
55
61
|
// one scrollable box for both axes (and the horizontal scrollbar visible).
|
|
56
62
|
const pinnedScroll = usePinnedScroll({
|
|
@@ -66,7 +72,7 @@
|
|
|
66
72
|
<div
|
|
67
73
|
data-streamdown-code={id}
|
|
68
74
|
data-incomplete={incomplete || undefined}
|
|
69
|
-
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
75
|
+
style={animate && streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
70
76
|
class={streamdown.theme.code.base}
|
|
71
77
|
>
|
|
72
78
|
<div class={streamdown.theme.code.header}>
|
|
@@ -109,13 +115,18 @@
|
|
|
109
115
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
110
116
|
<pre
|
|
111
117
|
class={streamdown.theme.code.pre}
|
|
118
|
+
data-line-numbers={lineNumbers.enabled || undefined}
|
|
119
|
+
style:counter-reset={lineNumbers.enabled ? `sd-line ${lineNumbers.start - 1}` : undefined}
|
|
112
120
|
style:max-height={streamdown.codeBlockMaxHeight}
|
|
113
121
|
style:overflow-y={streamdown.codeBlockMaxHeight ? 'auto' : undefined}
|
|
114
122
|
{@attach pinnedScroll}><code
|
|
115
|
-
>{#each lines as line}<span
|
|
123
|
+
>{#each lines as line}<span
|
|
124
|
+
class="{streamdown.theme.code.line}{lineNumbers.enabled
|
|
125
|
+
? ` ${streamdown.theme.code.lineNumber}`
|
|
126
|
+
: ''}"
|
|
116
127
|
>{#if line.length === 0}​{/if}{#each line as t}<span
|
|
117
128
|
class="th-token{t.className ? ` th-${t.className}` : ''}"
|
|
118
|
-
style={streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
129
|
+
style={animate && streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
119
130
|
style:color={streamdown.highlightTheme.tokens[t.className ?? 'token']}
|
|
120
131
|
>{t.value}</span
|
|
121
132
|
>{/each}</span
|
|
@@ -4,6 +4,8 @@ type $$ComponentProps = {
|
|
|
4
4
|
id: string;
|
|
5
5
|
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
6
|
incomplete?: boolean;
|
|
7
|
+
/** False for the render of a bulk update — a replacement or a paste-sized append (see Block). */
|
|
8
|
+
animate?: boolean;
|
|
7
9
|
};
|
|
8
10
|
declare const Code: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
11
|
type Code = ReturnType<typeof Code>;
|
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
let {
|
|
16
16
|
token,
|
|
17
17
|
children,
|
|
18
|
-
incomplete = false
|
|
19
|
-
|
|
18
|
+
incomplete = false,
|
|
19
|
+
animate = true
|
|
20
|
+
}: {
|
|
21
|
+
token: StreamdownToken;
|
|
22
|
+
children: Snippet;
|
|
23
|
+
incomplete?: boolean;
|
|
24
|
+
animate?: boolean;
|
|
25
|
+
} = $props();
|
|
20
26
|
const streamdown = useStreamdown();
|
|
21
27
|
|
|
22
28
|
// Use provided components or fallback to lightweight versions
|
|
@@ -25,9 +31,16 @@
|
|
|
25
31
|
const MathComponent = $derived(streamdown.components?.math ?? MathFallback);
|
|
26
32
|
|
|
27
33
|
// Only apply animation on block level elements. Leaves text elements to be animated by their text children.
|
|
28
|
-
const style = $derived(streamdown.isMounted ? streamdown.animationBlockStyle : '');
|
|
34
|
+
const style = $derived(animate && streamdown.isMounted ? streamdown.animationBlockStyle : '');
|
|
29
35
|
const id = $props.id();
|
|
30
36
|
|
|
37
|
+
// Owned here because the table wrapper below is what goes fullscreen, while
|
|
38
|
+
// TableDownload holds the toggle.
|
|
39
|
+
let tableExpanded = $state(false);
|
|
40
|
+
// One below the global [data-expanded='true'] rule, so the download toolbar —
|
|
41
|
+
// which precedes this wrapper in the DOM — can still paint over the overlay.
|
|
42
|
+
const expandedZIndex = 2147483646;
|
|
43
|
+
|
|
31
44
|
// Only ever attached to the table wrapper, which is the element that already
|
|
32
45
|
// owns the horizontal scroll.
|
|
33
46
|
const tableScroll = usePinnedScroll({
|
|
@@ -91,11 +104,11 @@
|
|
|
91
104
|
props={{ children, token, incomplete }}
|
|
92
105
|
render={streamdown.snippets.mermaid ?? streamdown.snippets.code}
|
|
93
106
|
>
|
|
94
|
-
<MermaidComponent {id} {token} {incomplete} />
|
|
107
|
+
<MermaidComponent {id} {token} {incomplete} {animate} />
|
|
95
108
|
</Slot>
|
|
96
109
|
{:else if token.type === 'code'}
|
|
97
110
|
<Slot props={{ children, token, incomplete }} render={streamdown.snippets.code}>
|
|
98
|
-
<CodeComponent {id} {token} {incomplete} />
|
|
111
|
+
<CodeComponent {id} {token} {incomplete} {animate} />
|
|
99
112
|
</Slot>
|
|
100
113
|
{:else if token.type === 'codespan'}
|
|
101
114
|
<Slot props={{ children, token }} render={streamdown.snippets.codespan}>
|
|
@@ -143,17 +156,25 @@
|
|
|
143
156
|
</Slot>
|
|
144
157
|
{:else if token.type === 'table'}
|
|
145
158
|
<Slot props={{ token, children }} render={streamdown.snippets.table}>
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
<!-- The toolbar stays a sibling of the wrapper and is only repositioned
|
|
160
|
+
while expanded, so the wrapper is a `role="dialog"` but deliberately
|
|
161
|
+
NOT `aria-modal`: that would mark the toolbar's own buttons inert for
|
|
162
|
+
any AT that honours it. Add it together with a focus trap. -->
|
|
163
|
+
{#if streamdown.controls.tableCopy || streamdown.controls.tableDownload || streamdown.controls.tableFullscreen}
|
|
164
|
+
<TableDownload {id} {token} bind:expanded={tableExpanded} />
|
|
148
165
|
{/if}
|
|
149
166
|
<div
|
|
150
167
|
data-streamdown-table={id}
|
|
151
168
|
{style}
|
|
152
|
-
class={`${streamdown.theme.table.base} group`}
|
|
169
|
+
class={`${streamdown.theme.table.base} group ${tableExpanded ? streamdown.theme.table.expanded : ''}`}
|
|
153
170
|
style:overscroll-behavior-x="none"
|
|
154
|
-
style:max-height={streamdown.tableMaxHeight}
|
|
155
|
-
style:overflow-y={streamdown.tableMaxHeight ? 'auto' : undefined}
|
|
171
|
+
style:max-height={tableExpanded ? undefined : streamdown.tableMaxHeight}
|
|
172
|
+
style:overflow-y={streamdown.tableMaxHeight && !tableExpanded ? 'auto' : undefined}
|
|
173
|
+
style:z-index={tableExpanded ? expandedZIndex : undefined}
|
|
156
174
|
{@attach tableScroll}
|
|
175
|
+
tabindex="-1"
|
|
176
|
+
role={tableExpanded ? 'dialog' : undefined}
|
|
177
|
+
aria-label={tableExpanded ? streamdown.translations.controls.table : undefined}
|
|
157
178
|
>
|
|
158
179
|
<table class={streamdown.theme.table.table}>
|
|
159
180
|
{@render children()}
|
|
@@ -284,7 +305,7 @@
|
|
|
284
305
|
<Slot props={{ children, token }} render={streamdown.snippets.descriptionList}>
|
|
285
306
|
<dl
|
|
286
307
|
data-streamdown-description-list={id}
|
|
287
|
-
style={streamdown.animationBlockStyle}
|
|
308
|
+
style={animate ? streamdown.animationBlockStyle : ''}
|
|
288
309
|
class={streamdown.theme.descriptionList.base}
|
|
289
310
|
>
|
|
290
311
|
{@render children()}
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
const {
|
|
14
14
|
token,
|
|
15
15
|
id,
|
|
16
|
-
incomplete = false
|
|
16
|
+
incomplete = false,
|
|
17
|
+
animate = true
|
|
17
18
|
}: {
|
|
18
19
|
token: CodeToken;
|
|
19
20
|
id: string;
|
|
20
21
|
/** The fence is still being streamed; nothing below it is final yet. */
|
|
21
22
|
incomplete?: boolean;
|
|
23
|
+
animate?: boolean;
|
|
22
24
|
} = $props();
|
|
23
25
|
|
|
24
26
|
// Trailing blank lines are noise for mermaid but they still changed `token.text`
|
|
@@ -251,7 +253,7 @@
|
|
|
251
253
|
{#if mermaid}
|
|
252
254
|
<div
|
|
253
255
|
bind:this={container}
|
|
254
|
-
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
256
|
+
style={animate && streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
255
257
|
class={streamdown.theme.mermaid.base}
|
|
256
258
|
{@attach (node) => {
|
|
257
259
|
// A half-written diagram makes mermaid throw and log on every chunk, so
|
|
@@ -321,15 +323,7 @@
|
|
|
321
323
|
</div>
|
|
322
324
|
|
|
323
325
|
<style>
|
|
324
|
-
|
|
325
|
-
position: fixed;
|
|
326
|
-
top: 16px;
|
|
327
|
-
left: 16px;
|
|
328
|
-
width: calc(100vw - 32px);
|
|
329
|
-
height: calc(100vh - 32px);
|
|
330
|
-
z-index: 2147483647;
|
|
331
|
-
margin: 0px;
|
|
332
|
-
}
|
|
326
|
+
/* [data-expanded='true'] now lives in Streamdown.svelte's global block. */
|
|
333
327
|
|
|
334
328
|
/* Hide Mermaid's temporary rendering containers */
|
|
335
329
|
:global(div[id^='dmermaid-']) {
|
|
@@ -4,6 +4,7 @@ type $$ComponentProps = {
|
|
|
4
4
|
id: string;
|
|
5
5
|
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
6
|
incomplete?: boolean;
|
|
7
|
+
animate?: boolean;
|
|
7
8
|
};
|
|
8
9
|
declare const Mermaid: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
10
|
type Mermaid = ReturnType<typeof Mermaid>;
|