svelte-streamdown 3.0.1 → 3.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 CHANGED
@@ -73,7 +73,6 @@ Full support for
73
73
  > [!NOTE]
74
74
  > 🧠 **AI Prompting Tip:** For best results, use our [comprehensive prompt](/prompting) covering all supported markdown features.
75
75
 
76
-
77
76
  ### 💻 Interactive Code Blocks
78
77
 
79
78
  - Syntax highlighting powered by Shiki
@@ -82,16 +81,28 @@ Full support for
82
81
 
83
82
  ### 🔢 Mathematical Expressions
84
83
 
85
- LaTeX math support through KaTeX:
84
+ LaTeX math support through KaTeX. Use single dollars for **inline** math and double dollars for **block** (display) math:
86
85
 
87
- - Perfect rendering for scientific content
88
- - Inline math: $E = mc^2$
86
+ - Inline math: `$E = mc^2$` renders inline as $E = mc^2$
89
87
  - Block math:
90
88
 
91
89
  $$
92
- \\sum_{i=1}^n x_i
90
+ f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}
93
91
  $$
94
92
 
93
+ KaTeX is an **opt-in** heavy component, so you must import the `Math` component and pass it via the `components` prop. Without it, math is rendered as raw text:
94
+
95
+ ```svelte
96
+ <script>
97
+ import { Streamdown } from 'svelte-streamdown';
98
+ import Math from 'svelte-streamdown/math'; // KaTeX math rendering
99
+ </script>
100
+
101
+ <Streamdown {content} components={{ math: Math }} />
102
+ ```
103
+
104
+ Pass KaTeX options through the [`katexConfig`](#-props-api) prop (e.g. to set `throwOnError` or macros). See [Bundle Optimization](#-bundle-optimization) for details on enabling heavy components.
105
+
95
106
  ### 🧜‍♀️ Mermaid Diagrams
96
107
 
97
108
  - Render Mermaid diagrams from code blocks
@@ -99,7 +110,6 @@ $$
99
110
  - Pan and Zoom
100
111
  - Full screen mode
101
112
 
102
-
103
113
  # **Example:**
104
114
 
105
115
  ```mermaid
@@ -140,11 +150,11 @@ pie title Project Time Allocation
140
150
 
141
151
  | H1 | H2 | H3 |
142
152
  | ------------------------- | --- | --- |
143
- | This cell spans 3 columns |||
153
+ | This cell spans 3 columns | | |
144
154
 
145
155
  | Header 1 | Header 2 | Header 3 |
146
156
  | ------------------------- | -------- | -------- |
147
- | This cell spans 2 columns || Normal |
157
+ | This cell spans 2 columns | | Normal |
148
158
  | Normal | Normal | Normal |
149
159
 
150
160
  #### Rowspan
@@ -160,7 +170,7 @@ pie title Project Time Allocation
160
170
  | --------------- | -------- |
161
171
  | Cell B | Cell A |
162
172
  | --------------- | -------- |
163
- | Footer ||
173
+ | Footer | |
164
174
 
165
175
  #### Column alignment
166
176
 
@@ -255,23 +265,23 @@ To enable inline citations, pass a `sources` object as a prop to the `Streamdown
255
265
 
256
266
  ```svelte
257
267
  <script>
258
- import { Streamdown } from 'svelte-streamdown';
259
-
260
- let content = `According to [smith2023], AI is advancing rapidly. See also [nested.subsection] for related work.`;
261
-
262
- let sources = {
263
- "smith2023": {
264
- title: "AI Research Paper",
265
- url: "https://example.com/paper",
266
- content: "Detailed content of the citation..."
267
- },
268
- "nested": {
269
- "subsection": {
270
- title: "Nested Citation",
271
- url: "https://example.com/nested"
272
- }
273
- }
274
- };
268
+ import { Streamdown } from 'svelte-streamdown';
269
+
270
+ let content = `According to [smith2023], AI is advancing rapidly. See also [nested.subsection] for related work.`;
271
+
272
+ let sources = {
273
+ smith2023: {
274
+ title: 'AI Research Paper',
275
+ url: 'https://example.com/paper',
276
+ content: 'Detailed content of the citation...'
277
+ },
278
+ nested: {
279
+ subsection: {
280
+ title: 'Nested Citation',
281
+ url: 'https://example.com/nested'
282
+ }
283
+ }
284
+ };
275
285
  </script>
276
286
 
277
287
  <Streamdown {content} {sources} />
@@ -285,7 +295,6 @@ Citations work with objects containing these properties:
285
295
  - `url (or href, url, link or source)`: Link to the source
286
296
  - `content (or text, summary or excerpt)`: Rich content to display in carousel mode
287
297
 
288
-
289
298
  #### Display Modes
290
299
 
291
300
  Streamdown offers two ways to display citations:
@@ -306,6 +315,7 @@ You can control the display mode using the `inlineCitationsMode` prop:
306
315
  #### Citation Popovers
307
316
 
308
317
  Citations appear as clickable buttons that open popovers when clicked. The popover shows:
318
+
309
319
  - Source title and URL (when available)
310
320
  - Favicon from the source domain
311
321
  - Rich content (in carousel mode)
@@ -317,22 +327,23 @@ If your citation data structure doesn't match the default format, you can custom
317
327
 
318
328
  ```svelte
319
329
  <Streamdown {content} {sources}>
320
- {#snippet inlineCitationPreview({ token })}
321
- <!-- Customize the clickable citation button -->
322
- {token.keys[0]}
323
- {/snippet}
324
-
325
- {#snippet inlineCitationContent({ source, key, token })}
326
- <!-- Customize content displayed in popover -->
327
- <div class="custom-content">
328
- <h4>{source.customTitle || key}</h4>
329
- <p>{source.customDescription}</p>
330
- </div>
331
- {/snippet}
330
+ {#snippet inlineCitationPreview({ token })}
331
+ <!-- Customize the clickable citation button -->
332
+ {token.keys[0]}
333
+ {/snippet}
334
+
335
+ {#snippet inlineCitationContent({ source, key, token })}
336
+ <!-- Customize content displayed in popover -->
337
+ <div class="custom-content">
338
+ <h4>{source.customTitle || key}</h4>
339
+ <p>{source.customDescription}</p>
340
+ </div>
341
+ {/snippet}
332
342
  </Streamdown>
333
343
  ```
334
344
 
335
345
  These snippets allow you to:
346
+
336
347
  - **`inlineCitationPreview`**: Customize the content of the clickable button that appears in the text
337
348
  - **`inlineCitationContent`**: Customize how individual citation content is displayed within popovers
338
349
  - **`inlineCitationPopover`**: Completely customize the list of citations
@@ -341,15 +352,16 @@ These snippets allow you to:
341
352
 
342
353
  This Svelte port maintains feature parity with the original [Streamdown](https://streamdown.ai/) while adapting to Svelte's patterns:
343
354
 
344
- | Aspect | Original (React) | Svelte Port |
345
- | ----------------- | ------------------------ | ------------------------- |
346
- | **Framework** | React | Svelte 5 |
347
- | **Component API** | JSX Components | Svelte Snippets |
348
- | **Styling** | Tailwind CSS | Tailwind CSS (compatible) |
349
- | **Context** | React Context | Svelte Context |
350
- | **Build System** | Vite/React | Vite/SvelteKit |
351
- | **TypeScript** | Full TS support | Full TS support |
352
- | **Engine** | Remark / Rehype + marked | marked only |
355
+ | Aspect | Original (React) | Svelte Port |
356
+ | ----------------- | ------------------------ | ---------------------------------------- |
357
+ | **Framework** | React | Svelte 5 |
358
+ | **Component API** | JSX Components | Svelte Snippets |
359
+ | **Styling** | Tailwind CSS | Tailwind CSS (compatible) |
360
+ | **Context** | React Context | Svelte Context |
361
+ | **Build System** | Vite/React | Vite/SvelteKit |
362
+ | **TypeScript** | Full TS support | Full TS support |
363
+ | **Engine** | Remark / Rehype + marked | marked only |
364
+ | **Memoization** | Memoized `Block` (LRU) | Svelte reactivity (per-block `$derived`) |
353
365
 
354
366
  ### Tailwind CSS Setup
355
367
 
@@ -365,6 +377,32 @@ This Svelte port maintains feature parity with the original [Streamdown](https:/
365
377
  @source "../node_modules/svelte-streamdown/**/*";
366
378
  ```
367
379
 
380
+ > [!IMPORTANT]
381
+ > The `@source` path is **relative to the stylesheet file** that contains the directive, so adjust the number of `../` segments to match where your stylesheet lives. The example above assumes `src/app.css`. If your global stylesheet lives one level deeper (e.g. `src/routes/+layout.css`, the default in newer SvelteKit projects), add one more `../` so the glob still resolves to your project's `node_modules`:
382
+
383
+ ```css
384
+ @import 'tailwindcss';
385
+ /* Add Streamdown styles to your Tailwind build (stylesheet inside src/routes) */
386
+ @source "../../node_modules/svelte-streamdown/**/*";
387
+ ```
388
+
389
+ ## ⚡ Streaming Performance & Memoization
390
+
391
+ Like the original [Streamdown](https://streamdown.ai/), `svelte-streamdown` avoids re-parsing the whole document on every streaming update — but it achieves this through Svelte 5's fine-grained reactivity rather than an explicit parse cache.
392
+
393
+ Here is how it works on each content update:
394
+
395
+ 1. **Block splitting**: the incoming `content` is split into top-level markdown blocks with `parseBlocks`. This is a lightweight lexer pass that only computes each block's raw string.
396
+ 2. **Keyed rendering**: blocks are rendered with a keyed `{#each}`, so existing block components are preserved across updates instead of being torn down and recreated.
397
+ 3. **Per-block memoized lexing**: each block component derives its tokens from its own raw string (`const tokens = $derived(lex(...))`). A Svelte `$derived` only recomputes when its inputs change, so a block is only re-lexed (the expensive inline tokenization step) when **its own** raw string changes.
398
+
399
+ During streaming, newly received text almost always only changes the **last** block (and occasionally starts a new one). Every earlier block keeps an identical raw string, so Svelte skips its `lex()` call entirely — this is the equivalent of the memoized `Block` component in the React version. The block-splitting pass itself runs on every update, but it is the cheap pass; the costly inline parsing is what gets reused.
400
+
401
+ Code highlighting is incremental as well: a code block is only re-highlighted when its text changes, and the Shiki highlighter caches the languages and themes it has already loaded, so a streaming code block does not reload its grammar on every chunk.
402
+
403
+ > [!NOTE]
404
+ > 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
+
368
406
  ## 🎭 Animation System
369
407
 
370
408
  Streamdown includes an animation system designed specifically for streaming AI content, providing smooth and engaging visual feedback as text appears on screen.
@@ -463,6 +501,19 @@ This heading will use a custom component!`;
463
501
  />
464
502
  ```
465
503
 
504
+ Prefixes can also be **protocol-only**, which allows any URL using that protocol. For example, `'https://'` allows every HTTPS link while still blocking insecure `http://` links, and `'mailto:'` / `'tel:'` allow email and phone links:
505
+
506
+ ```svelte
507
+ <Streamdown
508
+ {content}
509
+ allowedLinkPrefixes={['https://', 'mailto:']}
510
+ allowedImagePrefixes={['https://']}
511
+ />
512
+ ```
513
+
514
+ > [!NOTE]
515
+ > `'*'` allows all `http://` and `https://` URLs. 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
+
466
517
  ## 📦 Bundle Optimization
467
518
 
468
519
  Streamdown is optimized for minimal bundle size by making heavy components **opt-in**. By default, Code blocks, Mermaid diagrams, and Math expressions render as lightweight fallbacks (plain text). To enable full functionality, import and pass the components you need:
@@ -473,24 +524,21 @@ Streamdown is optimized for minimal bundle size by making heavy components **opt
473
524
  <script>
474
525
  import { Streamdown } from 'svelte-streamdown';
475
526
  // Import only the components you need
476
- import Code from 'svelte-streamdown/code'; // Shiki syntax highlighting
527
+ import Code from 'svelte-streamdown/code'; // Shiki syntax highlighting
477
528
  import Mermaid from 'svelte-streamdown/mermaid'; // Mermaid diagrams
478
- import Math from 'svelte-streamdown/math'; // KaTeX math rendering
529
+ import Math from 'svelte-streamdown/math'; // KaTeX math rendering
479
530
  </script>
480
531
 
481
- <Streamdown
482
- {content}
483
- components={{ code: Code, mermaid: Mermaid, math: Math }}
484
- />
532
+ <Streamdown {content} components={{ code: Code, mermaid: Mermaid, math: Math }} />
485
533
  ```
486
534
 
487
535
  ### Component Dependencies
488
536
 
489
- | Component | Import Path | Dependency | Size Impact |
490
- |-----------|-------------|------------|-------------|
491
- | `Code` | `svelte-streamdown/code` | Shiki | ~2MB (languages + themes) |
492
- | `Mermaid` | `svelte-streamdown/mermaid` | Mermaid.js | ~1.5MB |
493
- | `Math` | `svelte-streamdown/math` | KaTeX | ~300KB |
537
+ | Component | Import Path | Dependency | Size Impact |
538
+ | --------- | --------------------------- | ---------- | ------------------------- |
539
+ | `Code` | `svelte-streamdown/code` | Shiki | ~2MB (languages + themes) |
540
+ | `Mermaid` | `svelte-streamdown/mermaid` | Mermaid.js | ~1.5MB |
541
+ | `Math` | `svelte-streamdown/math` | KaTeX | ~300KB |
494
542
 
495
543
  > [!TIP]
496
544
  > Only import the components your application actually uses. If your content doesn't include code blocks, mermaid diagrams, or math expressions, you can skip those imports entirely for a much smaller bundle.
@@ -498,43 +546,111 @@ Streamdown is optimized for minimal bundle size by making heavy components **opt
498
546
  ### Fallback Behavior
499
547
 
500
548
  When a heavy component is not provided:
549
+
501
550
  - **Code blocks**: Render as plain `<pre><code>` without syntax highlighting
502
551
  - **Mermaid**: Renders the mermaid source as a code block
503
552
  - **Math**: Renders the raw LaTeX/math text
504
553
 
554
+ ### Shiki Themes
555
+
556
+ The `Code` component bundles two themes out of the box: **`github-dark`** and **`github-light`**. By default `shikiTheme` follows the active color scheme (`github-dark` in dark mode, `github-light` otherwise), so basic light/dark theming works with no extra configuration.
557
+
558
+ To use any other Shiki theme (e.g. `vesper`, `github-dark-default`, `github-light-default`), import it from `@shikijs/themes/<name>` and register it via the `shikiThemes` prop. The **key** you register it under is the value you pass to `shikiTheme`:
559
+
560
+ ```svelte
561
+ <script lang="ts">
562
+ import { Streamdown } from 'svelte-streamdown';
563
+ import Code from 'svelte-streamdown/code'; // enables Shiki highlighting
564
+ import vesper from '@shikijs/themes/vesper';
565
+
566
+ let { content } = $props();
567
+ </script>
568
+
569
+ <Streamdown {content} components={{ code: Code }} shikiThemes={{ vesper }} shikiTheme="vesper" />
570
+ ```
571
+
572
+ > [!IMPORTANT]
573
+ > A theme passed to `shikiTheme` must be one of the two built-in themes **or** registered via `shikiThemes`. If it is neither, the code block stays in its loading (skeleton) state and is never highlighted — this is the most common cause of "my theme stopped working".
574
+
575
+ #### Dynamic (light/dark) theme switching
576
+
577
+ Register every theme you intend to switch between in `shikiThemes`, then drive `shikiTheme` from your color-scheme store. Switching is fully reactive — code blocks re-highlight when `shikiTheme` changes:
578
+
579
+ ```svelte
580
+ <script lang="ts">
581
+ import { Streamdown } from 'svelte-streamdown';
582
+ import Code from 'svelte-streamdown/code';
583
+ import { mode } from 'mode-watcher';
584
+ import githubDarkDefault from '@shikijs/themes/github-dark-default';
585
+ import githubLightDefault from '@shikijs/themes/github-light-default';
586
+
587
+ let { content } = $props();
588
+
589
+ const shikiTheme = $derived(
590
+ mode.current === 'dark' ? 'github-dark-default' : 'github-light-default'
591
+ );
592
+ </script>
593
+
594
+ <Streamdown
595
+ {content}
596
+ components={{ code: Code }}
597
+ baseTheme="shadcn"
598
+ shikiThemes={{
599
+ 'github-dark-default': githubDarkDefault,
600
+ 'github-light-default': githubLightDefault
601
+ }}
602
+ {shikiTheme}
603
+ />
604
+ ```
605
+
606
+ > [!NOTE]
607
+ > The built-in `github-dark` / `github-light` themes can be switched dynamically with just `shikiTheme` (no `shikiThemes` registration needed), since both are always loaded.
608
+
609
+ #### Migrating from v2 (`shikiPreloadThemes`)
610
+
611
+ The v2 `shikiPreloadThemes` prop has been **removed**. Themes are no longer referenced by bundled name; instead you import the theme objects yourself and register them with `shikiThemes`. Registered themes are loaded together with the highlighter (i.e. effectively preloaded), so there is no separate preload step:
612
+
613
+ ```diff
614
+ - shikiPreloadThemes={['github-dark-default', 'github-light-default']}
615
+ + shikiThemes={{ 'github-dark-default': githubDarkDefault, 'github-light-default': githubLightDefault }}
616
+ ```
617
+
618
+ This also keeps the default bundle small: only the themes you actually import are included.
619
+
505
620
  ## 📋 Props API
506
621
 
507
- | Prop | Type | Default | Description |
508
- | -------------------------- | -------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
509
- | `content` | `string` | - | **Required.** The markdown content to render |
510
- | `sources` | `Record<string, any>` | - | Citation data object for inline citations |
511
- | `class` | `string` | - | CSS class names for the wrapper element |
512
- | `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax |
513
- | `defaultOrigin` | `string` | - | Default origin for relative URLs |
514
- | `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links |
515
- | `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images |
516
- | `skipHtml` | `boolean` | - | Skip HTML parsing entirely |
517
- | `unwrapDisallowed` | `boolean` | - | Unwrap instead of removing disallowed elements |
518
- | `urlTransform` | `UrlTransform \| null` | - | Custom URL transformation function |
519
- | `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
520
- | `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
521
- | `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
522
- | `shikiTheme` | `string` | `'github-light'` | Code highlighting theme (`github-dark` or `github-light` by default, or custom theme key) |
523
- | `shikiThemes` | `Record<string, ThemeRegistration>` | - | Additional themes as pre-imported objects (e.g., `{ nord: nordTheme }`) |
524
- | `shikiLanguages` | `LanguageInfo[]` | - | Additional syntax highlighting languages (merged with defaults) |
525
- | `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
526
- | `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
527
- | `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
528
- | `animation.enabled` | `boolean` | `false` | Enable/disable animations |
529
- | `animation.type` | `'fade' \| 'blur' \| 'typewriter' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance |
530
- | `animation.duration` | `number` | `500` | Animation duration in milliseconds |
531
- | `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations |
532
- | `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
533
- | `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 |
534
- | `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
535
- | `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`) |
536
- | `components` | `{ code?, mermaid?, math? }` | - | Optional heavy components for syntax highlighting, diagrams, and math rendering |
537
- | `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
622
+ | Prop | Type | Default | Description |
623
+ | -------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
624
+ | `content` | `string` | - | **Required.** The markdown content to render |
625
+ | `sources` | `Record<string, any>` | - | Citation data object for inline citations |
626
+ | `class` | `string` | - | CSS class names for the wrapper element |
627
+ | `parseIncompleteMarkdown` | `boolean` | `true` | Parse and fix incomplete markdown syntax |
628
+ | `defaultOrigin` | `string` | - | Default origin for relative URLs |
629
+ | `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links |
630
+ | `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images |
631
+ | `skipHtml` | `boolean` | - | Skip HTML parsing entirely |
632
+ | `unwrapDisallowed` | `boolean` | - | Unwrap instead of removing disallowed elements |
633
+ | `urlTransform` | `UrlTransform \| null` | - | Custom URL transformation function |
634
+ | `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
635
+ | `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
636
+ | `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
637
+ | `shikiTheme` | `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 `shikiThemes`. See [Shiki Themes](#shiki-themes). |
638
+ | `shikiThemes` | `Record<string, ThemeRegistration>` | - | Register additional pre-imported themes (e.g. `{ vesper }`) so they can be selected via `shikiTheme`, including dynamic light/dark switching. Replaces the v2 `shikiPreloadThemes` prop. |
639
+ | `shikiLanguages` | `LanguageInfo[]` | - | Additional syntax highlighting languages (merged with defaults) |
640
+ | `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
641
+ | `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
642
+ | `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
643
+ | `animation.enabled` | `boolean` | `false` | Enable/disable animations |
644
+ | `animation.type` | `'fade' \| 'blur' \| 'typewriter' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance |
645
+ | `animation.duration` | `number` | `500` | Animation duration in milliseconds |
646
+ | `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations |
647
+ | `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
648
+ | `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 |
649
+ | `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
650
+ | `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`) |
651
+ | `components` | `{ code?, mermaid?, math? }` | - | Optional heavy components for syntax highlighting, diagrams, and math rendering |
652
+ | `controls` | `{ code?: boolean, mermaid?: boolean \| { enabled?: boolean, mouseWheelZoom?: boolean }, table?: boolean }` | all `true` | Toggle the action toolbars for code blocks, mermaid diagrams, and tables. For mermaid, pass an object to disable only mouse-wheel zoom while keeping pan and the zoom buttons, e.g. `{ mermaid: { mouseWheelZoom: false } }` |
653
+ | `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
538
654
 
539
655
  #### All Available Customizable Elements:
540
656
 
@@ -667,9 +783,9 @@ Streamdown supports MDX-style JSX components, allowing you to embed custom Svelt
667
783
 
668
784
  ```svelte
669
785
  <script>
670
- import { Streamdown } from 'svelte-streamdown';
671
-
672
- let content = `
786
+ import { Streamdown } from 'svelte-streamdown';
787
+
788
+ let content = `
673
789
  # Using MDX Components
674
790
 
675
791
  <Card title="Hello" count={42}>
@@ -681,23 +797,23 @@ This is **markdown content** inside a component!
681
797
  </script>
682
798
 
683
799
  <Streamdown {content}>
684
- {#snippet mdx({ token, props, children })}
685
- {#if token.tagName === 'Card'}
686
- <div class="rounded-lg border border-gray-200 p-4 shadow-sm">
687
- <h3 class="text-xl font-bold">{props.title}</h3>
688
- <p class="text-gray-600">Count: {props.count}</p>
689
- <div class="mt-2">
690
- {@render children()}
691
- </div>
692
- </div>
693
- {:else if token.tagName === 'Button'}
694
- <button class="rounded px-4 py-2 {props.active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
695
- {props.label}
696
- </button>
697
- {:else}
698
- {@render children()}
699
- {/if}
700
- {/snippet}
800
+ {#snippet mdx({ token, props, children })}
801
+ {#if token.tagName === 'Card'}
802
+ <div class="rounded-lg border border-gray-200 p-4 shadow-sm">
803
+ <h3 class="text-xl font-bold">{props.title}</h3>
804
+ <p class="text-gray-600">Count: {props.count}</p>
805
+ <div class="mt-2">
806
+ {@render children()}
807
+ </div>
808
+ </div>
809
+ {:else if token.tagName === 'Button'}
810
+ <button class="rounded px-4 py-2 {props.active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
811
+ {props.label}
812
+ </button>
813
+ {:else}
814
+ {@render children()}
815
+ {/if}
816
+ {/snippet}
701
817
  </Streamdown>
702
818
  ```
703
819
 
@@ -707,11 +823,11 @@ Instead of using the `mdx` snippet with conditional logic, you can pass Svelte c
707
823
 
708
824
  ```svelte
709
825
  <script>
710
- import { Streamdown } from 'svelte-streamdown';
711
- import Card from './Card.svelte';
712
- import Button from './Button.svelte';
713
-
714
- let content = `
826
+ import { Streamdown } from 'svelte-streamdown';
827
+ import Card from './Card.svelte';
828
+ import Button from './Button.svelte';
829
+
830
+ let content = `
715
831
  # Using MDX Components
716
832
 
717
833
  <Card title="Hello" count={42}>
@@ -730,26 +846,26 @@ This is **markdown content** inside a component!
730
846
  ```svelte
731
847
  <!-- Card.svelte -->
732
848
  <script>
733
- let { title, count, children } = $props();
849
+ let { title, count, children } = $props();
734
850
  </script>
735
851
 
736
852
  <div class="rounded-lg border border-gray-200 p-4 shadow-sm">
737
- <h3 class="text-xl font-bold">{title}</h3>
738
- <p class="text-gray-600">Count: {count}</p>
739
- <div class="mt-2">
740
- {@render children()}
741
- </div>
853
+ <h3 class="text-xl font-bold">{title}</h3>
854
+ <p class="text-gray-600">Count: {count}</p>
855
+ <div class="mt-2">
856
+ {@render children()}
857
+ </div>
742
858
  </div>
743
859
  ```
744
860
 
745
861
  ```svelte
746
862
  <!-- Button.svelte -->
747
863
  <script>
748
- let { label, active } = $props();
864
+ let { label, active } = $props();
749
865
  </script>
750
866
 
751
867
  <button class="rounded px-4 py-2 {active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
752
- {label}
868
+ {label}
753
869
  </button>
754
870
  ```
755
871
 
@@ -758,11 +874,13 @@ This approach is cleaner when you have standalone component files, while the `md
758
874
  ### Supported Syntax
759
875
 
760
876
  **Self-closing components:**
877
+
761
878
  ```markdown
762
879
  <Component attr="value" count={42} enabled={true} />
763
880
  ```
764
881
 
765
882
  **Components with markdown children:**
883
+
766
884
  ```markdown
767
885
  <Component title="Hello">
768
886
  # This is a heading
@@ -798,31 +916,33 @@ This ensures your UI remains stable even when receiving partial markdown from st
798
916
  ### Component Props
799
917
 
800
918
  The `mdx` snippet receives three parameters:
919
+
801
920
  - `token`: The full MdxToken with `tagName`, `attributes`, `selfClosing`, etc.
802
921
  - `props`: Object containing all parsed attributes (e.g., `props.title`, `props.count`)
803
922
  - `children`: Snippet containing parsed markdown content
804
923
 
805
924
  Use `token.tagName` to determine which component is being rendered:
806
925
  <Card title="Hello" count={5}>Content</Card>
926
+
807
927
  ```svelte
808
928
  <!-- Markdown: <Card title="Hello" count={5}>Content</Card> -->
809
929
  <Streamdown {content}>
810
- {#snippet mdx({ token, props, children })}
811
- {#if token.tagName === 'Card'}
812
- <div>
813
- <h3>{props.title}</h3>
814
- <span>Count: {props.count}</span>
815
- {@render children()}
816
- </div>
817
- {:else if token.tagName === 'Alert'}
818
- <div class="alert alert-{props.type}">
819
- {@render children()}
820
- </div>
821
- {:else}
822
- <!-- Fallback for unknown components -->
823
- {@render children()}
824
- {/if}
825
- {/snippet}
930
+ {#snippet mdx({ token, props, children })}
931
+ {#if token.tagName === 'Card'}
932
+ <div>
933
+ <h3>{props.title}</h3>
934
+ <span>Count: {props.count}</span>
935
+ {@render children()}
936
+ </div>
937
+ {:else if token.tagName === 'Alert'}
938
+ <div class="alert alert-{props.type}">
939
+ {@render children()}
940
+ </div>
941
+ {:else}
942
+ <!-- Fallback for unknown components -->
943
+ {@render children()}
944
+ {/if}
945
+ {/snippet}
826
946
  </Streamdown>
827
947
  ```
828
948
 
@@ -37,8 +37,6 @@
37
37
  popover.isOpen = false;
38
38
  }
39
39
  });
40
-
41
- $inspect(token);
42
40
  </script>
43
41
 
44
42
  {#if popover.isOpen}
@@ -58,7 +58,7 @@
58
58
  maxZoom: 4,
59
59
  zoomSpeed: 1,
60
60
  get activateMouseWheel() {
61
- return insider.isInside;
61
+ return insider.isInside && streamdown.controls.mermaidMouseWheelZoom;
62
62
  }
63
63
  });
64
64