svelte-streamdown 4.0.1 → 4.1.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 +206 -41
- package/dist/Block.svelte +5 -2
- package/dist/Block.svelte.d.ts +2 -0
- package/dist/Elements/Alert.svelte +2 -1
- package/dist/Elements/Citation.svelte +7 -0
- package/dist/Elements/Code.svelte +55 -19
- package/dist/Elements/Code.svelte.d.ts +2 -0
- package/dist/Elements/Element.svelte +28 -6
- package/dist/Elements/Element.svelte.d.ts +1 -0
- package/dist/Elements/FootnoteRef.svelte +1 -0
- package/dist/Elements/Image.svelte +3 -2
- package/dist/Elements/Link.svelte +2 -2
- package/dist/Elements/Mermaid.svelte +62 -12
- package/dist/Elements/Mermaid.svelte.d.ts +2 -0
- package/dist/Elements/MermaidDownload.svelte +29 -8
- package/dist/Elements/MermaidDownload.svelte.d.ts +2 -0
- package/dist/Elements/TableDownload.svelte +57 -83
- package/dist/Elements/fallbacks/CodeFallback.svelte +21 -2
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +2 -0
- package/dist/Elements/fallbacks/MermaidFallback.svelte +5 -2
- package/dist/Elements/fallbacks/MermaidFallback.svelte.d.ts +2 -0
- package/dist/Elements/icons.js +10 -1
- package/dist/Elements/srOnly.d.ts +1 -0
- package/dist/Elements/srOnly.js +3 -0
- package/dist/Streamdown.svelte +68 -13
- package/dist/context.svelte.d.ts +98 -22
- package/dist/context.svelte.js +38 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/marked/index.js +25 -10
- package/dist/marked/marked-math.js +40 -1
- package/dist/utils/fence.d.ts +16 -0
- package/dist/utils/fence.js +39 -0
- package/dist/utils/parse-incomplete-markdown.d.ts +5 -1
- package/dist/utils/parse-incomplete-markdown.js +281 -138
- package/dist/utils/table-export.d.ts +14 -0
- package/dist/utils/table-export.js +82 -0
- package/dist/utils/usePinnedScroll.svelte.d.ts +22 -0
- package/dist/utils/usePinnedScroll.svelte.js +36 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ Perfect for AI-powered applications that need to stream and render markdown cont
|
|
|
26
26
|
- **Progressive Rendering**: Perfect for streaming AI responses
|
|
27
27
|
- **Real-time Updates**: Optimized for dynamic content
|
|
28
28
|
- **Smooth Animations**: Animate tokens and blocks as they are streamed.
|
|
29
|
+
- **An [`incomplete` signal](#the-incomplete-signal)** on the block still being streamed, so
|
|
30
|
+
expensive renderers can wait and loading states need no JavaScript
|
|
31
|
+
- **Capped, self-scrolling blocks**: `codeBlockMaxHeight` / `tableMaxHeight` keep a long snippet or
|
|
32
|
+
table pinned to its newest line while it streams
|
|
29
33
|
|
|
30
34
|
### 🔒 Security Hardening
|
|
31
35
|
|
|
@@ -58,7 +62,7 @@ Full support for
|
|
|
58
62
|
- Task lists ([ ] and [x])
|
|
59
63
|
- Code blocks
|
|
60
64
|
- Mermaid diagrams
|
|
61
|
-
- Math $expressions
|
|
65
|
+
- Math $expressions$, in `$…$` / `$$…$$` or the LaTeX `\(…\)` / `\[…\]` delimiters
|
|
62
66
|
- Escaping currency symbols ($140)
|
|
63
67
|
- Complex tables
|
|
64
68
|
- Footnotes [^1]
|
|
@@ -77,14 +81,17 @@ Full support for
|
|
|
77
81
|
|
|
78
82
|
- Syntax highlighting powered by [@tanstack/highlight](https://github.com/TanStack/highlight) (synchronous, SSR-friendly, ~31KB min / ~11KB gzip for every language)
|
|
79
83
|
- Copy-to-clipboard functionality
|
|
84
|
+
- Download the snippet with the extension of its language
|
|
80
85
|
- Support any `@tanstack/highlight` theme, or your own
|
|
81
86
|
|
|
82
87
|
### 🔢 Mathematical Expressions
|
|
83
88
|
|
|
84
|
-
LaTeX math support through KaTeX.
|
|
89
|
+
LaTeX math support through KaTeX. Both delimiter styles are supported — dollars and the LaTeX delimiters LLMs usually emit:
|
|
85
90
|
|
|
86
|
-
- Inline math: `$E = mc^2$` renders inline as $E = mc^2$
|
|
87
|
-
- Block math:
|
|
91
|
+
- Inline math: `$E = mc^2$` or `\(E = mc^2\)` renders inline as $E = mc^2$
|
|
92
|
+
- Block (display) math: `$$ … $$` or `\[ … \]`
|
|
93
|
+
|
|
94
|
+
> `\(` and `\[` are always read as math delimiters — in static rendering as well as while streaming. Prose that uses them as literal-bracket escapes renders as math: `\[optional\]` is a display-math token, and an unterminated `Use \[ to open a bracket` is auto-closed into one by the completer (`parseIncompleteMarkdown={false}` stops that half). Escape the backslash — `\\[` — to keep the sequence literal. See the [4.1.0 behaviour changes](CHANGELOG.md#410).
|
|
88
95
|
|
|
89
96
|
$$
|
|
90
97
|
f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}
|
|
@@ -109,6 +116,7 @@ Pass KaTeX options through the [`katexConfig`](#-props-api) prop (e.g. to set `t
|
|
|
109
116
|
- **Incremental rendering** during streaming content
|
|
110
117
|
- Pan and Zoom
|
|
111
118
|
- Full screen mode
|
|
119
|
+
- Download as PNG, SVG or `.mmd` source
|
|
112
120
|
|
|
113
121
|
# **Example:**
|
|
114
122
|
|
|
@@ -146,6 +154,9 @@ pie title Project Time Allocation
|
|
|
146
154
|
|
|
147
155
|
### Complex table support
|
|
148
156
|
|
|
157
|
+
Tables copy and download as Markdown, HTML, CSV or TSV — see [Controls](#-controls) for the
|
|
158
|
+
separator and filename options, and for the exported table utilities.
|
|
159
|
+
|
|
149
160
|
#### Colspan
|
|
150
161
|
|
|
151
162
|
| H1 | H2 | H3 |
|
|
@@ -403,6 +414,47 @@ Code highlighting is incremental as well: a code block is only re-highlighted wh
|
|
|
403
414
|
> [!NOTE]
|
|
404
415
|
> There is intentionally no separate block-level parse cache (e.g. an LRU keyed by block content). For the common append-only streaming case the reactivity-based approach above already avoids redundant work, and a standalone cache would add memory usage and invalidation complexity without a measurable benefit. If you have a workload where this matters, please [open an issue](https://github.com/beynar/svelte-streamdown/issues) with a repro — we're happy to revisit.
|
|
405
416
|
|
|
417
|
+
### The incremental block cache contract
|
|
418
|
+
|
|
419
|
+
Step 1 above keeps a small per-instance cache so that block splitting costs O(new text) rather than O(document) on each update. It seals every block except the last two and re-splits only the live tail.
|
|
420
|
+
|
|
421
|
+
Deciding whether an update is an append has to be cheap, so the sealed prefix is **sampled, not rescanned**: the first character of every sealed block, plus a fixed number of evenly spaced characters. Anything that fails a sample falls back to a full parse.
|
|
422
|
+
|
|
423
|
+
What that means in practice:
|
|
424
|
+
|
|
425
|
+
- **Append-only updates are exact.** This is what an LLM stream does, and what the component does with its own `content` prop.
|
|
426
|
+
- **Replacing, shortening or restructuring the content is detected** — a different length, a moved block boundary, or a changed block start all fail the checks and trigger a full reparse.
|
|
427
|
+
- **A same-length edit in the middle of a sealed block can be missed**, and that block will keep rendering its old text. If you bind `content` to an editor, or regenerate a block in the middle of a finished document, either pass `static` (which skips the streaming path) or force a fresh parse by re-keying the component:
|
|
428
|
+
|
|
429
|
+
```svelte
|
|
430
|
+
{#key documentVersion}
|
|
431
|
+
<Streamdown content={editorValue} />
|
|
432
|
+
{/key}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### The `incomplete` signal
|
|
436
|
+
|
|
437
|
+
While a fence is still streaming, the block it produces is a guess: the closing ``` has not arrived,
|
|
438
|
+
so the language, the last line and even whether it is a diagram at all can still change. Streamdown
|
|
439
|
+
now says so out loud.
|
|
440
|
+
|
|
441
|
+
- The `code` and `mermaid` snippets receive an extra `incomplete: boolean` prop, as do custom
|
|
442
|
+
`components.code` / `components.mermaid` components.
|
|
443
|
+
- The rendered container carries `data-incomplete="true"` while the fence is open, so a loading
|
|
444
|
+
style needs no JavaScript: `[data-streamdown-code][data-incomplete] { opacity: 0.7 }`.
|
|
445
|
+
- Only the **last** block of a streaming document can be incomplete, and `static` never marks
|
|
446
|
+
anything.
|
|
447
|
+
|
|
448
|
+
```svelte
|
|
449
|
+
{#snippet code({ token, children, incomplete })}
|
|
450
|
+
<pre class:animate-pulse={incomplete}>{@render children()}</pre>
|
|
451
|
+
{/snippet}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The built-in Mermaid component already acts on it: it skips `mermaid.render` while the fence is
|
|
455
|
+
open and keeps the last good diagram on screen, instead of re-parsing a half-written graph on every
|
|
456
|
+
chunk and flashing an error.
|
|
457
|
+
|
|
406
458
|
## 🎭 Animation System
|
|
407
459
|
|
|
408
460
|
Streamdown includes an animation system designed specifically for streaming AI content, providing smooth and engaging visual feedback as text appears on screen.
|
|
@@ -698,42 +750,44 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
698
750
|
|
|
699
751
|
## 📋 Props API
|
|
700
752
|
|
|
701
|
-
| Prop | Type
|
|
702
|
-
| -------------------------- |
|
|
703
|
-
| `content` | `string`
|
|
704
|
-
| `sources` | `Record<string, any>`
|
|
705
|
-
| `class` | `string`
|
|
706
|
-
| `parseIncompleteMarkdown` | `boolean`
|
|
707
|
-
| `defaultOrigin` | `string`
|
|
708
|
-
| `allowedLinkPrefixes` | `string[]`
|
|
709
|
-
| `allowedImagePrefixes` | `string[]`
|
|
710
|
-
| `renderHtml` | `boolean \| ((token) => string)`
|
|
711
|
-
| `inlineCitationsMode` | `'list' \| 'carousel'`
|
|
712
|
-
| `translations` | `{ alert?: {
|
|
713
|
-
| `icons` | `Partial<Record<IconName, Snippet>>`
|
|
714
|
-
| `static` | `boolean`
|
|
715
|
-
| `element` | `HTMLElement`
|
|
716
|
-
| `streamdown` | `StreamdownContext`
|
|
717
|
-
| `theme` | `DeepPartial<Theme>`
|
|
718
|
-
| `baseTheme` | `'tailwind' \| 'shadcn'`
|
|
719
|
-
| `mergeTheme` | `boolean`
|
|
720
|
-
| `highlightTheme` | `string`
|
|
721
|
-
| `highlightThemes` | `Record<string, HighlightTheme>`
|
|
722
|
-
| `highlightLanguages` | `LanguageDefinition[]`
|
|
723
|
-
| `mermaidConfig` | `MermaidConfig`
|
|
724
|
-
| `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)`
|
|
725
|
-
| `animation` | `AnimationConfig`
|
|
726
|
-
| `animation.enabled` | `boolean`
|
|
727
|
-
| `animation.type` | `'fade' \| 'blur' \| 'slideUp' \| 'slideDown'`
|
|
728
|
-
| `animation.duration` | `number`
|
|
729
|
-
| `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'`
|
|
730
|
-
| `animation.tokenize` | `'word' \| 'char'`
|
|
731
|
-
| `animation.animateOnMount` | `boolean`
|
|
732
|
-
| `extensions` | `Array<Extension>`
|
|
733
|
-
| `mdxComponents` | `Record<string, Component>`
|
|
734
|
-
| `components` | `{ code?, mermaid?, math? }`
|
|
735
|
-
| `controls` | `
|
|
736
|
-
| `
|
|
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
|
+
| `components` | `{ code?, mermaid?, math? }` | - | Optional heavy components for syntax highlighting, diagrams, and math rendering |
|
|
787
|
+
| `controls` | `boolean \| { code?, table?, mermaid? }` | all `true` | Toggle and configure the action toolbars for code blocks, tables and mermaid diagrams — see [Controls](#-controls) |
|
|
788
|
+
| `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. |
|
|
789
|
+
| `tableMaxHeight` | `string` | - | CSS length that caps the height of tables, with the same streaming auto-scroll as `codeBlockMaxHeight`. |
|
|
790
|
+
| `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
|
|
737
791
|
|
|
738
792
|
#### All Available Customizable Elements:
|
|
739
793
|
|
|
@@ -743,7 +797,7 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
743
797
|
|
|
744
798
|
**Lists**: `ul`, `ol`, `li`
|
|
745
799
|
|
|
746
|
-
**Code**: `code`, `codespan`
|
|
800
|
+
**Code**: `code`, `codespan` — `code` and `mermaid` also receive [`incomplete`](#the-incomplete-signal)
|
|
747
801
|
|
|
748
802
|
**Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`
|
|
749
803
|
|
|
@@ -753,6 +807,117 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
753
807
|
|
|
754
808
|
**Note**: The above elements are **supported by Streamdown** and should be customized using individual props or the theme system. MDX components require the `mdx` snippet.
|
|
755
809
|
|
|
810
|
+
## 🌍 Translations
|
|
811
|
+
|
|
812
|
+
Every string the components render themselves — alert titles, button labels, download menu entries, the copy announcements screen readers hear, the blocked-URL tooltips — comes from one nested `translations` object. Pass only the keys you want to change; the rest fall back to `defaultTranslations`, which is exported so you can read the shipped English values (or diff against them when a new key appears).
|
|
813
|
+
|
|
814
|
+
```svelte
|
|
815
|
+
<script>
|
|
816
|
+
import { Streamdown, defaultTranslations } from 'svelte-streamdown';
|
|
817
|
+
</script>
|
|
818
|
+
|
|
819
|
+
<Streamdown
|
|
820
|
+
{content}
|
|
821
|
+
translations={{
|
|
822
|
+
alert: { note: 'remarque', warning: 'attention' },
|
|
823
|
+
controls: {
|
|
824
|
+
copyCode: 'Copier le code',
|
|
825
|
+
copiedCode: 'Code copié',
|
|
826
|
+
downloadCode: 'Télécharger le code'
|
|
827
|
+
}
|
|
828
|
+
}}
|
|
829
|
+
/>
|
|
830
|
+
```
|
|
831
|
+
|
|
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` |
|
|
836
|
+
|
|
837
|
+
The alert defaults are lowercase because the theme capitalizes them with CSS; if you drop that class, capitalize them here instead.
|
|
838
|
+
|
|
839
|
+
Control labels are used as both the `title` and the `aria-label` of the matching icon-only button, so translating them translates the accessible name too. `copiedCode` / `copiedTable` are announced through a visually hidden `aria-live` region after a copy.
|
|
840
|
+
|
|
841
|
+
## 🎛️ Controls
|
|
842
|
+
|
|
843
|
+
`controls` turns the code / table / mermaid toolbars on and off and configures what they do.
|
|
844
|
+
`controls={false}` turns every control off, `controls={true}` (the default) turns them all on,
|
|
845
|
+
and each section takes a boolean or an object:
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
type Controls =
|
|
849
|
+
| boolean
|
|
850
|
+
| {
|
|
851
|
+
code?:
|
|
852
|
+
| boolean
|
|
853
|
+
| {
|
|
854
|
+
enabled?: boolean;
|
|
855
|
+
copy?: boolean;
|
|
856
|
+
download?: boolean | { filename?: string | ((token: CodeToken) => string) };
|
|
857
|
+
};
|
|
858
|
+
table?:
|
|
859
|
+
| boolean
|
|
860
|
+
| {
|
|
861
|
+
enabled?: boolean;
|
|
862
|
+
copy?: boolean;
|
|
863
|
+
download?: boolean | { filename?: string | ((token: TableToken) => string) };
|
|
864
|
+
csvSeparator?: ',' | ';' | '\t' | 'auto';
|
|
865
|
+
};
|
|
866
|
+
mermaid?:
|
|
867
|
+
| boolean
|
|
868
|
+
| {
|
|
869
|
+
enabled?: boolean;
|
|
870
|
+
download?: boolean | { filename?: string | ((token: CodeToken) => string) };
|
|
871
|
+
mouseWheelZoom?: boolean;
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
```svelte
|
|
877
|
+
<Streamdown
|
|
878
|
+
{content}
|
|
879
|
+
controls={{
|
|
880
|
+
code: { download: { filename: (token) => `snippet-${token.lang}` } },
|
|
881
|
+
table: { copy: false, csvSeparator: 'auto' },
|
|
882
|
+
mermaid: { mouseWheelZoom: false }
|
|
883
|
+
}}
|
|
884
|
+
/>
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
- `filename` is the **base name**; the extension still comes from the content — `languageExtensionMap`
|
|
888
|
+
for code (`.ts`, `.py`, … `.txt`), `.csv` / `.tsv` / `.md` / `.html` for tables, `.svg` / `.png` / `.mmd`
|
|
889
|
+
for diagrams. The defaults are `file`, `table` and `diagram`.
|
|
890
|
+
- `csvSeparator: 'auto'` picks `;` when the browser locale writes decimals with a comma (Excel reads
|
|
891
|
+
`,` as a decimal point there), otherwise `,`. CSV downloads carry a UTF-8 BOM so accented and CJK
|
|
892
|
+
text opens correctly in Excel.
|
|
893
|
+
- `mouseWheelZoom` is a gesture rather than a button: it stays on unless you set it to `false` or
|
|
894
|
+
turn every control off with `controls={false}`.
|
|
895
|
+
|
|
896
|
+
### Table export utilities
|
|
897
|
+
|
|
898
|
+
The table toolbar's DOM walk is exported, so a custom `table` snippet can build its own copy or
|
|
899
|
+
download menu:
|
|
900
|
+
|
|
901
|
+
```ts
|
|
902
|
+
import {
|
|
903
|
+
extractTableData,
|
|
904
|
+
tableDataToCSV,
|
|
905
|
+
tableDataToTSV,
|
|
906
|
+
tableDataToMarkdown,
|
|
907
|
+
tableDataToHTML,
|
|
908
|
+
type TableData
|
|
909
|
+
} from 'svelte-streamdown';
|
|
910
|
+
|
|
911
|
+
const data = extractTableData(document.querySelector('[data-streamdown-table="..."]')!);
|
|
912
|
+
// { headers: string[], rows: string[][] } — <br> becomes \n, colspan/rowspan become empty cells
|
|
913
|
+
tableDataToCSV(data, ';');
|
|
914
|
+
tableDataToTSV(data);
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
`tableDataToMarkdown` and `tableDataToHTML` are there for callers that only hold a DOM table; the
|
|
918
|
+
built-in menu copies Markdown from `token.raw` instead, which keeps the author's original inline
|
|
919
|
+
formatting.
|
|
920
|
+
|
|
756
921
|
## 🎨 Theming System
|
|
757
922
|
|
|
758
923
|
### Built-in Themes
|
package/dist/Block.svelte
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
let {
|
|
10
10
|
block,
|
|
11
|
-
static: isStatic = false
|
|
11
|
+
static: isStatic = false,
|
|
12
|
+
incomplete = false
|
|
12
13
|
}: {
|
|
13
14
|
block: string;
|
|
14
15
|
static?: boolean;
|
|
16
|
+
/** This block ends inside an unfinished code fence (streaming only). */
|
|
17
|
+
incomplete?: boolean;
|
|
15
18
|
} = $props();
|
|
16
19
|
|
|
17
20
|
const streamdown = useStreamdown();
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
{#if token}
|
|
30
33
|
{@const children = (token as any)?.tokens || []}
|
|
31
34
|
{@const isTextOnlyNode = children.length === 0}
|
|
32
|
-
<Element {token}>
|
|
35
|
+
<Element {token} {incomplete}>
|
|
33
36
|
{#if isTextOnlyNode}
|
|
34
37
|
{#if streamdown.animation.enabled && !insidePopover && !isStatic}
|
|
35
38
|
<AnimatedText text={'text' in token ? token.text || '' : ''} />
|
package/dist/Block.svelte.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type $$ComponentProps = {
|
|
2
2
|
block: string;
|
|
3
3
|
static?: boolean;
|
|
4
|
+
/** This block ends inside an unfinished code fence (streaming only). */
|
|
5
|
+
incomplete?: boolean;
|
|
4
6
|
};
|
|
5
7
|
declare const Block: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
8
|
type Block = ReturnType<typeof Block>;
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
>
|
|
46
46
|
<div data-alert-title class={streamdown.theme.alert.title}>
|
|
47
47
|
{@render (streamdown.icons?.[token.variant] || icon)()}
|
|
48
|
-
{streamdown.translations
|
|
48
|
+
{streamdown.translations.alert[token.variant]}
|
|
49
49
|
</div>
|
|
50
50
|
{@render children()}
|
|
51
51
|
</div>
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
|
|
54
54
|
{#snippet icon()}
|
|
55
55
|
<svg
|
|
56
|
+
aria-hidden="true"
|
|
56
57
|
xmlns="http://www.w3.org/2000/svg"
|
|
57
58
|
width="24"
|
|
58
59
|
height="24"
|
|
@@ -152,6 +152,9 @@
|
|
|
152
152
|
disabled={!stepper.canGoPrevious}
|
|
153
153
|
class={streamdown.theme.components.button}
|
|
154
154
|
onclick={() => stepper.previous()}
|
|
155
|
+
type="button"
|
|
156
|
+
aria-label={streamdown.translations.controls.previousCitation}
|
|
157
|
+
title={streamdown.translations.controls.previousCitation}
|
|
155
158
|
>
|
|
156
159
|
{@render (streamdown.icons?.chevronLeft || chevronLeft)()}
|
|
157
160
|
</button>
|
|
@@ -159,6 +162,9 @@
|
|
|
159
162
|
disabled={!stepper.canGoNext}
|
|
160
163
|
class={streamdown.theme.components.button}
|
|
161
164
|
onclick={() => stepper.next()}
|
|
165
|
+
type="button"
|
|
166
|
+
aria-label={streamdown.translations.controls.nextCitation}
|
|
167
|
+
title={streamdown.translations.controls.nextCitation}
|
|
162
168
|
>
|
|
163
169
|
{@render (streamdown.icons?.chevronRight || chevronRight)()}
|
|
164
170
|
</button>
|
|
@@ -243,6 +249,7 @@
|
|
|
243
249
|
aria-expanded={popover.isOpen}
|
|
244
250
|
aria-haspopup="dialog"
|
|
245
251
|
aria-controls={'citation-popover-' + id}
|
|
252
|
+
type="button"
|
|
246
253
|
{@attach clickOutside.attachment}
|
|
247
254
|
>
|
|
248
255
|
<Slot
|
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
import { useCopy } from '../utils/copy.svelte.js';
|
|
5
5
|
import { highlightLines, languageExtensionMap } from '../utils/hightlighter.svelte.js';
|
|
6
6
|
import type { CodeToken } from '../marked/index.js';
|
|
7
|
+
import { usePinnedScroll } from '../utils/usePinnedScroll.svelte.js';
|
|
7
8
|
import { checkIcon, copyIcon, downloadIcon } from './icons.js';
|
|
9
|
+
import { srOnly } from './srOnly.js';
|
|
8
10
|
|
|
9
11
|
const {
|
|
10
12
|
token,
|
|
11
|
-
id
|
|
13
|
+
id,
|
|
14
|
+
incomplete = false
|
|
12
15
|
}: {
|
|
13
16
|
token: CodeToken;
|
|
14
17
|
id: string;
|
|
18
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
19
|
+
incomplete?: boolean;
|
|
15
20
|
} = $props();
|
|
16
21
|
|
|
17
22
|
const streamdown = useStreamdown();
|
|
@@ -35,7 +40,8 @@
|
|
|
35
40
|
token.lang && token.lang in languageExtensionMap
|
|
36
41
|
? languageExtensionMap[token.lang as keyof typeof languageExtensionMap]
|
|
37
42
|
: 'txt';
|
|
38
|
-
const
|
|
43
|
+
const base = streamdown.controls.codeDownloadFilename;
|
|
44
|
+
const filename = `${typeof base === 'function' ? base(token) : base}.${extension}`;
|
|
39
45
|
const mimeType = 'text/plain';
|
|
40
46
|
save(filename, code, mimeType);
|
|
41
47
|
} catch (error) {
|
|
@@ -44,38 +50,68 @@
|
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
const lines = $derived(highlightLines(code, token.lang, streamdown.highlightLanguages));
|
|
53
|
+
|
|
54
|
+
// `pre` is already the horizontal scroll container, so capping it there keeps
|
|
55
|
+
// one scrollable box for both axes (and the horizontal scrollbar visible).
|
|
56
|
+
const pinnedScroll = usePinnedScroll({
|
|
57
|
+
get maxHeight() {
|
|
58
|
+
return streamdown.codeBlockMaxHeight;
|
|
59
|
+
},
|
|
60
|
+
get content() {
|
|
61
|
+
return code;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
47
64
|
</script>
|
|
48
65
|
|
|
49
66
|
<div
|
|
50
67
|
data-streamdown-code={id}
|
|
68
|
+
data-incomplete={incomplete || undefined}
|
|
51
69
|
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
52
70
|
class={streamdown.theme.code.base}
|
|
53
71
|
>
|
|
54
72
|
<div class={streamdown.theme.code.header}>
|
|
55
73
|
<span class={streamdown.theme.code.language}>{token.lang}</span>
|
|
56
|
-
{#if streamdown.controls.
|
|
74
|
+
{#if streamdown.controls.codeCopy || streamdown.controls.codeDownload}
|
|
57
75
|
<div class={streamdown.theme.code.buttons}>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
{#if streamdown.controls.codeDownload}
|
|
77
|
+
<button
|
|
78
|
+
class={streamdown.theme.components.button}
|
|
79
|
+
onclick={downloadCode}
|
|
80
|
+
title={streamdown.translations.controls.downloadCode}
|
|
81
|
+
aria-label={streamdown.translations.controls.downloadCode}
|
|
82
|
+
type="button"
|
|
83
|
+
>
|
|
84
|
+
{@render (streamdown.icons?.download || downloadIcon)()}
|
|
85
|
+
</button>
|
|
86
|
+
{/if}
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
{#if streamdown.controls.codeCopy}
|
|
89
|
+
<button
|
|
90
|
+
class={streamdown.theme.components.button}
|
|
91
|
+
onclick={copy.copy}
|
|
92
|
+
title={streamdown.translations.controls.copyCode}
|
|
93
|
+
aria-label={streamdown.translations.controls.copyCode}
|
|
94
|
+
type="button"
|
|
95
|
+
>
|
|
96
|
+
{#if copy.isCopied}
|
|
97
|
+
{@render (streamdown.icons?.check || checkIcon)()}
|
|
98
|
+
{:else}
|
|
99
|
+
{@render (streamdown.icons?.copy || copyIcon)()}
|
|
100
|
+
{/if}
|
|
101
|
+
</button>
|
|
102
|
+
{/if}
|
|
103
|
+
<span aria-live="polite" style={srOnly}
|
|
104
|
+
>{copy.isCopied ? streamdown.translations.controls.copiedCode : ''}</span
|
|
105
|
+
>
|
|
74
106
|
</div>
|
|
75
107
|
{/if}
|
|
76
108
|
</div>
|
|
77
109
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
78
|
-
<pre
|
|
110
|
+
<pre
|
|
111
|
+
class={streamdown.theme.code.pre}
|
|
112
|
+
style:max-height={streamdown.codeBlockMaxHeight}
|
|
113
|
+
style:overflow-y={streamdown.codeBlockMaxHeight ? 'auto' : undefined}
|
|
114
|
+
{@attach pinnedScroll}><code
|
|
79
115
|
>{#each lines as line}<span class={streamdown.theme.code.line}
|
|
80
116
|
>{#if line.length === 0}​{/if}{#each line as t}<span
|
|
81
117
|
class="th-token{t.className ? ` th-${t.className}` : ''}"
|
|
@@ -2,6 +2,8 @@ import type { CodeToken } from '../marked/index.js';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
token: CodeToken;
|
|
4
4
|
id: string;
|
|
5
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
|
+
incomplete?: boolean;
|
|
5
7
|
};
|
|
6
8
|
declare const Code: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
9
|
type Code = ReturnType<typeof Code>;
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
import FootnoteRef from './FootnoteRef.svelte';
|
|
10
10
|
import Citation from './Citation.svelte';
|
|
11
11
|
import TableDownload from './TableDownload.svelte';
|
|
12
|
+
import { usePinnedScroll } from '../utils/usePinnedScroll.svelte.js';
|
|
12
13
|
// Import fallback components
|
|
13
14
|
import { CodeFallback, MermaidFallback, MathFallback } from './fallbacks/index.js';
|
|
14
|
-
let {
|
|
15
|
+
let {
|
|
16
|
+
token,
|
|
17
|
+
children,
|
|
18
|
+
incomplete = false
|
|
19
|
+
}: { token: StreamdownToken; children: Snippet; incomplete?: boolean } = $props();
|
|
15
20
|
const streamdown = useStreamdown();
|
|
16
21
|
|
|
17
22
|
// Use provided components or fallback to lightweight versions
|
|
@@ -22,6 +27,17 @@
|
|
|
22
27
|
// Only apply animation on block level elements. Leaves text elements to be animated by their text children.
|
|
23
28
|
const style = $derived(streamdown.isMounted ? streamdown.animationBlockStyle : '');
|
|
24
29
|
const id = $props.id();
|
|
30
|
+
|
|
31
|
+
// Only ever attached to the table wrapper, which is the element that already
|
|
32
|
+
// owns the horizontal scroll.
|
|
33
|
+
const tableScroll = usePinnedScroll({
|
|
34
|
+
get maxHeight() {
|
|
35
|
+
return streamdown.tableMaxHeight;
|
|
36
|
+
},
|
|
37
|
+
get content() {
|
|
38
|
+
return token.type === 'table' ? token.raw : undefined;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
25
41
|
</script>
|
|
26
42
|
|
|
27
43
|
{#if token.type === 'heading'}
|
|
@@ -71,12 +87,15 @@
|
|
|
71
87
|
</blockquote>
|
|
72
88
|
</Slot>
|
|
73
89
|
{:else if token.type === 'code' && token.lang === 'mermaid'}
|
|
74
|
-
<Slot
|
|
75
|
-
|
|
90
|
+
<Slot
|
|
91
|
+
props={{ children, token, incomplete }}
|
|
92
|
+
render={streamdown.snippets.mermaid ?? streamdown.snippets.code}
|
|
93
|
+
>
|
|
94
|
+
<MermaidComponent {id} {token} {incomplete} />
|
|
76
95
|
</Slot>
|
|
77
96
|
{:else if token.type === 'code'}
|
|
78
|
-
<Slot props={{ children, token }} render={streamdown.snippets.code}>
|
|
79
|
-
<CodeComponent {id} {token} />
|
|
97
|
+
<Slot props={{ children, token, incomplete }} render={streamdown.snippets.code}>
|
|
98
|
+
<CodeComponent {id} {token} {incomplete} />
|
|
80
99
|
</Slot>
|
|
81
100
|
{:else if token.type === 'codespan'}
|
|
82
101
|
<Slot props={{ children, token }} render={streamdown.snippets.codespan}>
|
|
@@ -124,7 +143,7 @@
|
|
|
124
143
|
</Slot>
|
|
125
144
|
{:else if token.type === 'table'}
|
|
126
145
|
<Slot props={{ token, children }} render={streamdown.snippets.table}>
|
|
127
|
-
{#if streamdown.controls.
|
|
146
|
+
{#if streamdown.controls.tableCopy || streamdown.controls.tableDownload}
|
|
128
147
|
<TableDownload {id} {token} />
|
|
129
148
|
{/if}
|
|
130
149
|
<div
|
|
@@ -132,6 +151,9 @@
|
|
|
132
151
|
{style}
|
|
133
152
|
class={`${streamdown.theme.table.base} group`}
|
|
134
153
|
style:overscroll-behavior-x="none"
|
|
154
|
+
style:max-height={streamdown.tableMaxHeight}
|
|
155
|
+
style:overflow-y={streamdown.tableMaxHeight ? 'auto' : undefined}
|
|
156
|
+
{@attach tableScroll}
|
|
135
157
|
>
|
|
136
158
|
<table class={streamdown.theme.table.table}>
|
|
137
159
|
{@render children()}
|
|
@@ -3,6 +3,7 @@ import type { StreamdownToken } from '../marked/index.js';
|
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
token: StreamdownToken;
|
|
5
5
|
children: Snippet;
|
|
6
|
+
incomplete?: boolean;
|
|
6
7
|
};
|
|
7
8
|
declare const Element: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
9
|
type Element = ReturnType<typeof Element>;
|