svelte-streamdown 4.0.0 → 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 +211 -38
- package/dist/Block.svelte +10 -4
- package/dist/Block.svelte.d.ts +2 -0
- package/dist/Elements/Alert.svelte +2 -1
- package/dist/Elements/Citation.svelte +9 -2
- package/dist/Elements/Code.svelte +65 -24
- package/dist/Elements/Code.svelte.d.ts +4 -2
- package/dist/Elements/Element.svelte +36 -11
- 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 +3 -2
- package/dist/Elements/Mermaid.svelte +69 -14
- package/dist/Elements/Mermaid.svelte.d.ts +4 -2
- package/dist/Elements/MermaidDownload.svelte +30 -9
- package/dist/Elements/MermaidDownload.svelte.d.ts +2 -0
- package/dist/Elements/TableDownload.svelte +60 -78
- package/dist/Elements/fallbacks/CodeFallback.svelte +28 -3
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +2 -0
- package/dist/Elements/fallbacks/MermaidFallback.svelte +12 -3
- 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 +3 -2
- package/dist/index.js +2 -1
- package/dist/marked/index.d.ts +8 -1
- package/dist/marked/index.js +65 -14
- package/dist/marked/marked-footnotes.js +6 -2
- package/dist/marked/marked-math.js +40 -1
- package/dist/marked/marked-subsup.js +16 -3
- 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 +347 -122
- package/dist/utils/save.js +4 -1
- package/dist/utils/table-export.d.ts +14 -0
- package/dist/utils/table-export.js +82 -0
- package/dist/utils/url.js +6 -2
- package/dist/utils/usePinnedScroll.svelte.d.ts +22 -0
- package/dist/utils/usePinnedScroll.svelte.js +36 -0
- package/package.json +4 -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.
|
|
@@ -512,7 +564,7 @@ Prefixes can also be **protocol-only**, which allows any URL using that protocol
|
|
|
512
564
|
```
|
|
513
565
|
|
|
514
566
|
> [!NOTE]
|
|
515
|
-
> `'*'` allows
|
|
567
|
+
> `'*'` allows every `http:`, `https:`, `mailto:` and `tel:` URL — the protocols a document can legitimately link to. `javascript:`, `data:` and `vbscript:` stay blocked under the wildcard because they execute in the page's origin. A protocol-only prefix only allows that exact protocol, so list each one you want to permit. Only add a protocol you trust — e.g. do not add `'javascript:'`.
|
|
516
568
|
|
|
517
569
|
## 📦 Bundle Optimization
|
|
518
570
|
|
|
@@ -698,38 +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
|
-
| `
|
|
711
|
-
| `
|
|
712
|
-
| `
|
|
713
|
-
| `
|
|
714
|
-
| `
|
|
715
|
-
| `
|
|
716
|
-
| `
|
|
717
|
-
| `
|
|
718
|
-
| `
|
|
719
|
-
| `
|
|
720
|
-
| `
|
|
721
|
-
| `
|
|
722
|
-
| `
|
|
723
|
-
| `
|
|
724
|
-
| `
|
|
725
|
-
| `animation
|
|
726
|
-
| `animation.
|
|
727
|
-
| `animation.
|
|
728
|
-
| `
|
|
729
|
-
| `
|
|
730
|
-
| `
|
|
731
|
-
| `
|
|
732
|
-
| `
|
|
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 |
|
|
733
791
|
|
|
734
792
|
#### All Available Customizable Elements:
|
|
735
793
|
|
|
@@ -739,7 +797,7 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
739
797
|
|
|
740
798
|
**Lists**: `ul`, `ol`, `li`
|
|
741
799
|
|
|
742
|
-
**Code**: `code`, `
|
|
800
|
+
**Code**: `code`, `codespan` — `code` and `mermaid` also receive [`incomplete`](#the-incomplete-signal)
|
|
743
801
|
|
|
744
802
|
**Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`
|
|
745
803
|
|
|
@@ -749,6 +807,117 @@ v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, r
|
|
|
749
807
|
|
|
750
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.
|
|
751
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
|
+
|
|
752
921
|
## 🎨 Theming System
|
|
753
922
|
|
|
754
923
|
### Built-in Themes
|
|
@@ -1103,6 +1272,10 @@ pnpm dev
|
|
|
1103
1272
|
# Run tests
|
|
1104
1273
|
pnpm test
|
|
1105
1274
|
|
|
1275
|
+
# Run the browser (component) tests — needs a Chromium binary:
|
|
1276
|
+
# pnpm exec playwright install chromium
|
|
1277
|
+
pnpm test:browser
|
|
1278
|
+
|
|
1106
1279
|
# Build for production
|
|
1107
1280
|
pnpm build
|
|
1108
1281
|
```
|
package/dist/Block.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { parseIncompleteMarkdown } from './utils/parse-incomplete-markdown.js';
|
|
2
|
+
import { parseIncompleteMarkdown as completeMarkdown } from './utils/parse-incomplete-markdown.js';
|
|
3
3
|
import Element from './Elements/Element.svelte';
|
|
4
4
|
import { lex, type StreamdownToken } from './marked/index.js';
|
|
5
5
|
import AnimatedText from './AnimatedText.svelte';
|
|
@@ -8,15 +8,21 @@
|
|
|
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();
|
|
21
|
+
// The old code never consulted `streamdown.parseIncompleteMarkdown`; the import
|
|
22
|
+
// is aliased so the context flag and the helper cannot be confused.
|
|
23
|
+
const complete = $derived(!isStatic && streamdown.parseIncompleteMarkdown !== false);
|
|
18
24
|
const tokens = $derived(
|
|
19
|
-
lex(
|
|
25
|
+
lex(complete ? completeMarkdown(block.trim()) : block, streamdown.extensions)
|
|
20
26
|
);
|
|
21
27
|
const insidePopover = getContext('POPOVER');
|
|
22
28
|
</script>
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
{#if token}
|
|
27
33
|
{@const children = (token as any)?.tokens || []}
|
|
28
34
|
{@const isTextOnlyNode = children.length === 0}
|
|
29
|
-
<Element {token}>
|
|
35
|
+
<Element {token} {incomplete}>
|
|
30
36
|
{#if isTextOnlyNode}
|
|
31
37
|
{#if streamdown.animation.enabled && !insidePopover && !isStatic}
|
|
32
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>
|
|
@@ -174,7 +180,7 @@
|
|
|
174
180
|
style:position="relative"
|
|
175
181
|
style:transition-duration="200ms"
|
|
176
182
|
style:transition-timing-function="ease-in-out"
|
|
177
|
-
aria-label=
|
|
183
|
+
aria-label={'Citations-' + id}
|
|
178
184
|
>
|
|
179
185
|
<div
|
|
180
186
|
bind:this={stepper.stepContainer}
|
|
@@ -193,7 +199,7 @@
|
|
|
193
199
|
style:height="fit-content"
|
|
194
200
|
style:width="100%"
|
|
195
201
|
style:flex-grow="1"
|
|
196
|
-
aria-label=
|
|
202
|
+
aria-label={'Citation-' + id}
|
|
197
203
|
>
|
|
198
204
|
<Slot render={streamdown.snippets.inlineCitationContent} props={{ source, key, token }}>
|
|
199
205
|
{#if url || title}
|
|
@@ -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
|
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
import { save } from '../utils/save.js';
|
|
4
4
|
import { useCopy } from '../utils/copy.svelte.js';
|
|
5
5
|
import { highlightLines, languageExtensionMap } from '../utils/hightlighter.svelte.js';
|
|
6
|
-
import type {
|
|
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
|
-
token:
|
|
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();
|
|
18
23
|
|
|
24
|
+
// marked keeps the fence's trailing blank lines in `text`; they render as empty
|
|
25
|
+
// lines and, while streaming, flicker in and out on nearly every chunk. Render,
|
|
26
|
+
// copy and download all read this so they can never disagree.
|
|
27
|
+
const code = $derived(token.text.replace(/\n+$/, ''));
|
|
28
|
+
|
|
19
29
|
const copy = useCopy({
|
|
20
30
|
get content() {
|
|
21
|
-
return
|
|
31
|
+
return code;
|
|
22
32
|
}
|
|
23
33
|
});
|
|
24
34
|
|
|
@@ -30,47 +40,78 @@
|
|
|
30
40
|
token.lang && token.lang in languageExtensionMap
|
|
31
41
|
? languageExtensionMap[token.lang as keyof typeof languageExtensionMap]
|
|
32
42
|
: 'txt';
|
|
33
|
-
const
|
|
43
|
+
const base = streamdown.controls.codeDownloadFilename;
|
|
44
|
+
const filename = `${typeof base === 'function' ? base(token) : base}.${extension}`;
|
|
34
45
|
const mimeType = 'text/plain';
|
|
35
|
-
save(filename,
|
|
46
|
+
save(filename, code, mimeType);
|
|
36
47
|
} catch (error) {
|
|
37
48
|
console.error('Failed to download file:', error);
|
|
38
49
|
}
|
|
39
50
|
};
|
|
40
51
|
|
|
41
|
-
const lines = $derived(highlightLines(
|
|
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
|
+
});
|
|
42
64
|
</script>
|
|
43
65
|
|
|
44
66
|
<div
|
|
45
67
|
data-streamdown-code={id}
|
|
68
|
+
data-incomplete={incomplete || undefined}
|
|
46
69
|
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
47
70
|
class={streamdown.theme.code.base}
|
|
48
71
|
>
|
|
49
72
|
<div class={streamdown.theme.code.header}>
|
|
50
73
|
<span class={streamdown.theme.code.language}>{token.lang}</span>
|
|
51
|
-
{#if streamdown.controls.
|
|
74
|
+
{#if streamdown.controls.codeCopy || streamdown.controls.codeDownload}
|
|
52
75
|
<div class={streamdown.theme.code.buttons}>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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}
|
|
61
87
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
>
|
|
69
106
|
</div>
|
|
70
107
|
{/if}
|
|
71
108
|
</div>
|
|
72
109
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
73
|
-
<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
|
|
74
115
|
>{#each lines as line}<span class={streamdown.theme.code.line}
|
|
75
116
|
>{#if line.length === 0}​{/if}{#each line as t}<span
|
|
76
117
|
class="th-token{t.className ? ` th-${t.className}` : ''}"
|
|
@@ -1,7 +1,9 @@
|
|
|
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
|
+
/** 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>;
|