svelte-streamdown 4.1.1 → 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 +146 -47
- package/dist/Block.svelte +9 -2
- package/dist/Block.svelte.d.ts +2 -0
- package/dist/Elements/Code.svelte +9 -1
- package/dist/Elements/Element.svelte +20 -5
- package/dist/Elements/Mermaid.svelte +1 -9
- package/dist/Elements/TableDownload.svelte +44 -3
- package/dist/Elements/TableDownload.svelte.d.ts +3 -1
- package/dist/Elements/fallbacks/CodeFallback.svelte +11 -3
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +2 -2
- package/dist/Elements/icons.d.ts +1 -0
- package/dist/Elements/icons.js +8 -0
- package/dist/Streamdown.svelte +58 -3
- package/dist/context.svelte.d.ts +32 -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 +5 -4
- package/dist/marked/index.js +33 -44
- 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/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 +167 -65
- 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 |
|
|
@@ -753,44 +816,48 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
753
816
|
|
|
754
817
|
## 📋 Props API
|
|
755
818
|
|
|
756
|
-
| Prop | Type | Default | Description
|
|
757
|
-
| -------------------------- | -------------------------------------------------------------------------------- | ---------------------- |
|
|
758
|
-
| `content` | `string` | - | **Required.** The markdown content to render
|
|
759
|
-
| `sources` | `Record<string, any>` | - | Citation data object for inline citations
|
|
760
|
-
| `class` | `string` | - | CSS class names for the wrapper element
|
|
761
|
-
| `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax
|
|
762
|
-
| `defaultOrigin` | `string` | - | Default origin for relative URLs
|
|
763
|
-
| `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links
|
|
764
|
-
| `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images
|
|
765
|
-
| `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.
|
|
766
|
-
| `inlineCitationsMode` | `'list' \| 'carousel'` | `'carousel'` | How an inline citation popover presents its sources
|
|
767
|
-
| `translations` | `{ alert?: {...}, controls?: {...} }` | `defaultTranslations` | Override the built-in alert titles and control labels — see [Translations](#-translations)
|
|
768
|
-
| `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
|
|
769
|
-
| `static` | `boolean` | `false` | Render finished content: skips the incomplete-markdown pass and the streaming animation
|
|
770
|
-
| `element` | `HTMLElement` | - | `bind:element` to get the wrapper node
|
|
771
|
-
| `streamdown` | `StreamdownContext` | - | `bind:streamdown` to read the resolved context (theme, controls, footnotes, sources)
|
|
772
|
-
| `theme` | `DeepPartial<Theme>` | - | Custom theme overrides
|
|
773
|
-
| `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides
|
|
774
|
-
| `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme
|
|
775
|
-
| `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).
|
|
776
|
-
| `highlightThemes` | `Record<string, HighlightTheme>` | - | Register additional pre-imported themes (e.g. `{ dracula }`) so they can be selected via `highlightTheme`, including dynamic light/dark switching.
|
|
777
|
-
| `highlightLanguages` | `LanguageDefinition[]` | - | Additional languages built with `defineLanguage` (merged with the 30 built-in ones)
|
|
778
|
-
| `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration
|
|
779
|
-
| `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options
|
|
780
|
-
| `animation` | `AnimationConfig` | - | Animation configuration for streaming content
|
|
781
|
-
| `animation.enabled` | `boolean` | `false` | Enable/disable animations
|
|
782
|
-
| `animation.type` | `'fade' \| 'blur' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance
|
|
783
|
-
| `animation.duration` | `number` | `500` | Animation duration in milliseconds
|
|
784
|
-
| `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations
|
|
785
|
-
| `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations
|
|
786
|
-
| `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
|
|
787
|
-
| `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens
|
|
788
|
-
| `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`)
|
|
789
|
-
| `
|
|
790
|
-
| `
|
|
791
|
-
| `
|
|
792
|
-
| `
|
|
793
|
-
| `
|
|
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 |
|
|
794
861
|
|
|
795
862
|
#### All Available Customizable Elements:
|
|
796
863
|
|
|
@@ -832,10 +899,10 @@ Every string the components render themselves — alert titles, button labels, d
|
|
|
832
899
|
/>
|
|
833
900
|
```
|
|
834
901
|
|
|
835
|
-
| Namespace | Keys
|
|
836
|
-
| ---------- |
|
|
837
|
-
| `alert` | `note`, `tip`, `warning`, `caution`, `important`
|
|
838
|
-
| `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` |
|
|
839
906
|
|
|
840
907
|
The alert defaults are lowercase because the theme capitalizes them with CSS; if you drop that class, capitalize them here instead.
|
|
841
908
|
|
|
@@ -864,6 +931,7 @@ type Controls =
|
|
|
864
931
|
enabled?: boolean;
|
|
865
932
|
copy?: boolean;
|
|
866
933
|
download?: boolean | { filename?: string | ((token: TableToken) => string) };
|
|
934
|
+
fullscreen?: boolean;
|
|
867
935
|
csvSeparator?: ',' | ';' | '\t' | 'auto';
|
|
868
936
|
};
|
|
869
937
|
mermaid?:
|
|
@@ -895,6 +963,9 @@ type Controls =
|
|
|
895
963
|
text opens correctly in Excel.
|
|
896
964
|
- `mouseWheelZoom` is a gesture rather than a button: it stays on unless you set it to `false` or
|
|
897
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.
|
|
898
969
|
|
|
899
970
|
### Table export utilities
|
|
900
971
|
|
|
@@ -1004,13 +1075,13 @@ Each component supports multiple themeable parts:
|
|
|
1004
1075
|
|
|
1005
1076
|
**Links (`a`)**: `base`, `blocked` (for blocked/unsafe links)
|
|
1006
1077
|
|
|
1007
|
-
**Code (`code`)**: `base`, `container`, `header`, `buttons`, `language`, `line`, `pre`
|
|
1078
|
+
**Code (`code`)**: `base`, `container`, `header`, `buttons`, `language`, `line`, `lineNumber`, `pre`
|
|
1008
1079
|
|
|
1009
1080
|
**Inline Code (`inlineCode`)**: `base`
|
|
1010
1081
|
|
|
1011
1082
|
**Images (`img`)**: `container`, `base`, `downloadButton`
|
|
1012
1083
|
|
|
1013
|
-
**Tables (`table`, `thead`, `tbody`, `tr`, `th`, `td`)**: `base`, `
|
|
1084
|
+
**Tables (`table`, `thead`, `tbody`, `tr`, `th`, `td`)**: `base`, plus `table` and `expanded` (table only)
|
|
1014
1085
|
|
|
1015
1086
|
**Blockquotes (`blockquote`)**: `base`
|
|
1016
1087
|
|
|
@@ -1150,9 +1221,36 @@ MDX components support three attribute value types:
|
|
|
1150
1221
|
|
|
1151
1222
|
### Component Naming
|
|
1152
1223
|
|
|
1153
|
-
-
|
|
1154
|
-
-
|
|
1155
|
-
-
|
|
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.
|
|
1156
1254
|
|
|
1157
1255
|
### Streaming Safety
|
|
1158
1256
|
|
|
@@ -1161,6 +1259,7 @@ MDX components are streaming-safe. Incomplete components are automatically handl
|
|
|
1161
1259
|
- Incomplete tags like `<Component attr` not rendered to prevent runtime errors
|
|
1162
1260
|
- Unclosed components like `<Card>content` are auto-closed with `</Card>`
|
|
1163
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`.
|
|
1164
1263
|
|
|
1165
1264
|
This ensures your UI remains stable even when receiving partial markdown from streaming AI responses.
|
|
1166
1265
|
|
package/dist/Block.svelte
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
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();
|
|
@@ -22,7 +25,11 @@
|
|
|
22
25
|
// is aliased so the context flag and the helper cannot be confused.
|
|
23
26
|
const complete = $derived(!isStatic && streamdown.parseIncompleteMarkdown !== false);
|
|
24
27
|
const view = $derived.by(() => {
|
|
25
|
-
const tokens = lex(
|
|
28
|
+
const tokens = lex(
|
|
29
|
+
complete ? completeMarkdown(block.trim(), { tags: streamdown.tags, live }) : block,
|
|
30
|
+
streamdown.extensions,
|
|
31
|
+
streamdown.tags
|
|
32
|
+
);
|
|
26
33
|
// Decided when this block's text changes and deliberately not tracked: a
|
|
27
34
|
// bulk update renders plain, and the next streamed chunk must not
|
|
28
35
|
// retroactively animate the blocks it left untouched.
|
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,6 +5,7 @@
|
|
|
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
|
|
|
@@ -54,6 +55,8 @@
|
|
|
54
55
|
|
|
55
56
|
const lines = $derived(highlightLines(code, token.lang, streamdown.highlightLanguages));
|
|
56
57
|
|
|
58
|
+
const lineNumbers = $derived(resolveLineNumbers(token.meta, streamdown.lineNumbers ?? false));
|
|
59
|
+
|
|
57
60
|
// `pre` is already the horizontal scroll container, so capping it there keeps
|
|
58
61
|
// one scrollable box for both axes (and the horizontal scrollbar visible).
|
|
59
62
|
const pinnedScroll = usePinnedScroll({
|
|
@@ -112,10 +115,15 @@
|
|
|
112
115
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
113
116
|
<pre
|
|
114
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}
|
|
115
120
|
style:max-height={streamdown.codeBlockMaxHeight}
|
|
116
121
|
style:overflow-y={streamdown.codeBlockMaxHeight ? 'auto' : undefined}
|
|
117
122
|
{@attach pinnedScroll}><code
|
|
118
|
-
>{#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
|
+
: ''}"
|
|
119
127
|
>{#if line.length === 0}​{/if}{#each line as t}<span
|
|
120
128
|
class="th-token{t.className ? ` th-${t.className}` : ''}"
|
|
121
129
|
style={animate && streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
const style = $derived(animate && streamdown.isMounted ? streamdown.animationBlockStyle : '');
|
|
35
35
|
const id = $props.id();
|
|
36
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
|
+
|
|
37
44
|
// Only ever attached to the table wrapper, which is the element that already
|
|
38
45
|
// owns the horizontal scroll.
|
|
39
46
|
const tableScroll = usePinnedScroll({
|
|
@@ -149,17 +156,25 @@
|
|
|
149
156
|
</Slot>
|
|
150
157
|
{:else if token.type === 'table'}
|
|
151
158
|
<Slot props={{ token, children }} render={streamdown.snippets.table}>
|
|
152
|
-
|
|
153
|
-
|
|
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} />
|
|
154
165
|
{/if}
|
|
155
166
|
<div
|
|
156
167
|
data-streamdown-table={id}
|
|
157
168
|
{style}
|
|
158
|
-
class={`${streamdown.theme.table.base} group`}
|
|
169
|
+
class={`${streamdown.theme.table.base} group ${tableExpanded ? streamdown.theme.table.expanded : ''}`}
|
|
159
170
|
style:overscroll-behavior-x="none"
|
|
160
|
-
style:max-height={streamdown.tableMaxHeight}
|
|
161
|
-
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}
|
|
162
174
|
{@attach tableScroll}
|
|
175
|
+
tabindex="-1"
|
|
176
|
+
role={tableExpanded ? 'dialog' : undefined}
|
|
177
|
+
aria-label={tableExpanded ? streamdown.translations.controls.table : undefined}
|
|
163
178
|
>
|
|
164
179
|
<table class={streamdown.theme.table.table}>
|
|
165
180
|
{@render children()}
|
|
@@ -323,15 +323,7 @@
|
|
|
323
323
|
</div>
|
|
324
324
|
|
|
325
325
|
<style>
|
|
326
|
-
|
|
327
|
-
position: fixed;
|
|
328
|
-
top: 16px;
|
|
329
|
-
left: 16px;
|
|
330
|
-
width: calc(100vw - 32px);
|
|
331
|
-
height: calc(100vh - 32px);
|
|
332
|
-
z-index: 2147483647;
|
|
333
|
-
margin: 0px;
|
|
334
|
-
}
|
|
326
|
+
/* [data-expanded='true'] now lives in Streamdown.svelte's global block. */
|
|
335
327
|
|
|
336
328
|
/* Hide Mermaid's temporary rendering containers */
|
|
337
329
|
:global(div[id^='dmermaid-']) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useStreamdown } from '../context.svelte.js';
|
|
3
3
|
import { scale } from 'svelte/transition';
|
|
4
|
-
import { checkIcon, copyIcon, downloadIcon } from './icons.js';
|
|
4
|
+
import { checkIcon, closeIcon, copyIcon, downloadIcon, fullscreenIcon } from './icons.js';
|
|
5
5
|
import { Popover } from './popover.svelte.js';
|
|
6
6
|
import { useClickOutside } from '../utils/useClickOutside.svelte.js';
|
|
7
7
|
import { useKeyDown } from '../utils/useKeyDown.svelte.js';
|
|
8
|
+
import { useExpand } from '../utils/expand.svelte.js';
|
|
8
9
|
import type { TableToken } from '../marked/marked-table.js';
|
|
9
10
|
import { useCopy } from '../utils/copy.svelte.js';
|
|
10
11
|
import { save } from '../utils/save.js';
|
|
@@ -15,10 +16,13 @@
|
|
|
15
16
|
|
|
16
17
|
let {
|
|
17
18
|
token,
|
|
18
|
-
id
|
|
19
|
+
id,
|
|
20
|
+
expanded = $bindable(false)
|
|
19
21
|
}: {
|
|
20
22
|
token: TableToken;
|
|
21
23
|
id: string;
|
|
24
|
+
/** Owned by the caller: the wrapper it renders is what expands. */
|
|
25
|
+
expanded?: boolean;
|
|
22
26
|
} = $props();
|
|
23
27
|
const streamdown = useStreamdown();
|
|
24
28
|
const popover = new Popover();
|
|
@@ -76,7 +80,22 @@
|
|
|
76
80
|
}
|
|
77
81
|
};
|
|
78
82
|
|
|
79
|
-
const tableElement = () => document.querySelector(`[data-streamdown-table="${id}"]`);
|
|
83
|
+
const tableElement = () => document.querySelector<HTMLElement>(`[data-streamdown-table="${id}"]`);
|
|
84
|
+
|
|
85
|
+
const expand = useExpand({
|
|
86
|
+
get expanded() {
|
|
87
|
+
return expanded;
|
|
88
|
+
},
|
|
89
|
+
set expanded(value: boolean) {
|
|
90
|
+
expanded = value;
|
|
91
|
+
},
|
|
92
|
+
getTarget: tableElement
|
|
93
|
+
});
|
|
94
|
+
const expandLabel = $derived(
|
|
95
|
+
expanded
|
|
96
|
+
? streamdown.translations.controls.exitTableFullscreen
|
|
97
|
+
: streamdown.translations.controls.tableFullscreen
|
|
98
|
+
);
|
|
80
99
|
|
|
81
100
|
const copyOrDownload = (type: Format) => {
|
|
82
101
|
if (type === 'Markdown') {
|
|
@@ -145,9 +164,15 @@
|
|
|
145
164
|
</dialog>
|
|
146
165
|
{/if}
|
|
147
166
|
|
|
167
|
+
<!-- The wrapper this toolbar controls becomes `position: fixed` when expanded,
|
|
168
|
+
so the buttons have to leave the flow with it or they end up underneath. -->
|
|
148
169
|
<div
|
|
149
170
|
data-streamdown-table-download
|
|
150
171
|
class=" right-0 ml-auto flex items-center justify-end gap-2 p-1"
|
|
172
|
+
style:position={expanded ? 'fixed' : undefined}
|
|
173
|
+
style:top={expanded ? '24px' : undefined}
|
|
174
|
+
style:right={expanded ? '32px' : undefined}
|
|
175
|
+
style:z-index={expanded ? 2147483647 : undefined}
|
|
151
176
|
>
|
|
152
177
|
{#each modes as mode (mode)}
|
|
153
178
|
<button
|
|
@@ -186,6 +211,22 @@
|
|
|
186
211
|
{/if}
|
|
187
212
|
</button>
|
|
188
213
|
{/each}
|
|
214
|
+
{#if streamdown.controls.tableFullscreen}
|
|
215
|
+
<button
|
|
216
|
+
class={streamdown.theme.components.button}
|
|
217
|
+
onclick={(e) => expand.toggle(e.currentTarget)}
|
|
218
|
+
type="button"
|
|
219
|
+
title={expandLabel}
|
|
220
|
+
aria-label={expandLabel}
|
|
221
|
+
aria-pressed={expanded}
|
|
222
|
+
>
|
|
223
|
+
{#if expanded}
|
|
224
|
+
{@render (streamdown.icons?.close || closeIcon)()}
|
|
225
|
+
{:else}
|
|
226
|
+
{@render (streamdown.icons?.fullscreen || fullscreenIcon)()}
|
|
227
|
+
{/if}
|
|
228
|
+
</button>
|
|
229
|
+
{/if}
|
|
189
230
|
<span aria-live="polite" style={srOnly}
|
|
190
231
|
>{copy.isCopied ? streamdown.translations.controls.copiedTable : ''}</span
|
|
191
232
|
>
|
|
@@ -2,7 +2,9 @@ import type { TableToken } from '../marked/marked-table.js';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
token: TableToken;
|
|
4
4
|
id: string;
|
|
5
|
+
/** Owned by the caller: the wrapper it renders is what expands. */
|
|
6
|
+
expanded?: boolean;
|
|
5
7
|
};
|
|
6
|
-
declare const TableDownload: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
declare const TableDownload: import("svelte").Component<$$ComponentProps, {}, "expanded">;
|
|
7
9
|
type TableDownload = ReturnType<typeof TableDownload>;
|
|
8
10
|
export default TableDownload;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useStreamdown } from '../../context.svelte.js';
|
|
3
3
|
import { usePinnedScroll } from '../../utils/usePinnedScroll.svelte.js';
|
|
4
|
-
import
|
|
4
|
+
import { resolveLineNumbers } from '../../utils/line-numbers.js';
|
|
5
|
+
import type { CodeToken } from '../../marked/index.js';
|
|
5
6
|
|
|
6
7
|
const {
|
|
7
8
|
token,
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
incomplete = false,
|
|
10
11
|
animate = true
|
|
11
12
|
}: {
|
|
12
|
-
token:
|
|
13
|
+
token: CodeToken;
|
|
13
14
|
id: string;
|
|
14
15
|
/** The fence is still being streamed; nothing below it is final yet. */
|
|
15
16
|
incomplete?: boolean;
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
// too.
|
|
25
26
|
const code = $derived(token.text.replace(/\n+$/, ''));
|
|
26
27
|
|
|
28
|
+
const lineNumbers = $derived(resolveLineNumbers(token.meta, streamdown.lineNumbers ?? false));
|
|
29
|
+
|
|
27
30
|
// Same scroll container as Code.svelte: `pre` already scrolls horizontally.
|
|
28
31
|
const pinnedScroll = usePinnedScroll({
|
|
29
32
|
get maxHeight() {
|
|
@@ -47,10 +50,15 @@
|
|
|
47
50
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
48
51
|
<pre
|
|
49
52
|
class={streamdown.theme.code.pre}
|
|
53
|
+
data-line-numbers={lineNumbers.enabled || undefined}
|
|
54
|
+
style:counter-reset={lineNumbers.enabled ? `sd-line ${lineNumbers.start - 1}` : undefined}
|
|
50
55
|
style:max-height={streamdown.codeBlockMaxHeight}
|
|
51
56
|
style:overflow-y={streamdown.codeBlockMaxHeight ? 'auto' : undefined}
|
|
52
57
|
{@attach pinnedScroll}><code
|
|
53
|
-
>{#each code.split('\n') as line}<span
|
|
58
|
+
>{#each code.split('\n') as line}<span
|
|
59
|
+
class="{streamdown.theme.code.line}{lineNumbers.enabled
|
|
60
|
+
? ` ${streamdown.theme.code.lineNumber}`
|
|
61
|
+
: ''}"
|
|
54
62
|
><span style={animate && streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
55
63
|
>{line.trim().length > 0 ? line : '\u200B'}</span
|
|
56
64
|
></span
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CodeToken } from '../../marked/index.js';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
|
-
token:
|
|
3
|
+
token: CodeToken;
|
|
4
4
|
id: string;
|
|
5
5
|
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
6
|
incomplete?: boolean;
|
package/dist/Elements/icons.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export declare const zoomInIcon: import("svelte").Snippet<[]>;
|
|
|
5
5
|
export declare const zoomOutIcon: import("svelte").Snippet<[]>;
|
|
6
6
|
export declare const fitViewIcon: import("svelte").Snippet<[]>;
|
|
7
7
|
export declare const fullscreenIcon: import("svelte").Snippet<[]>;
|
|
8
|
+
export declare const closeIcon: import("svelte").Snippet<[]>;
|
|
8
9
|
export declare const chevronRight: import("svelte").Snippet<[]>;
|
|
9
10
|
export declare const chevronLeft: import("svelte").Snippet<[]>;
|
package/dist/Elements/icons.js
CHANGED
|
@@ -142,6 +142,14 @@ export const fullscreenIcon = createRawSnippet(() => {
|
|
|
142
142
|
`
|
|
143
143
|
};
|
|
144
144
|
});
|
|
145
|
+
export const closeIcon = createRawSnippet(() => {
|
|
146
|
+
return {
|
|
147
|
+
render: () => `
|
|
148
|
+
<svg
|
|
149
|
+
aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
150
|
+
`
|
|
151
|
+
};
|
|
152
|
+
});
|
|
145
153
|
export const chevronRight = createRawSnippet(() => {
|
|
146
154
|
return {
|
|
147
155
|
render: () => `
|