svelte-streamdown 2.6.0 → 3.0.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 +135 -26
- package/dist/Block.svelte +8 -4
- package/dist/Block.svelte.d.ts +1 -0
- package/dist/Elements/Code.svelte +6 -1
- package/dist/Elements/Element.svelte +15 -14
- package/dist/Elements/Mermaid.svelte +2 -0
- package/dist/Elements/MermaidDownload.svelte +196 -0
- package/dist/Elements/MermaidDownload.svelte.d.ts +6 -0
- package/dist/Elements/fallbacks/CodeFallback.svelte +33 -0
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +8 -0
- package/dist/Elements/fallbacks/MathFallback.svelte +35 -0
- package/dist/Elements/fallbacks/MathFallback.svelte.d.ts +8 -0
- package/dist/Elements/fallbacks/MermaidFallback.svelte +34 -0
- package/dist/Elements/fallbacks/MermaidFallback.svelte.d.ts +8 -0
- package/dist/Elements/fallbacks/index.d.ts +3 -0
- package/dist/Elements/fallbacks/index.js +3 -0
- package/dist/Streamdown.svelte +41 -10
- package/dist/context.svelte.d.ts +31 -6
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/utils/bundledLanguages.d.ts +8 -0
- package/dist/utils/bundledLanguages.js +143 -0
- package/dist/utils/darkMode.svelte.d.ts +3 -0
- package/dist/utils/darkMode.svelte.js +49 -0
- package/dist/utils/hightlighter.svelte.d.ts +17 -19
- package/dist/utils/hightlighter.svelte.js +141 -61
- package/dist/utils/parse-incomplete-markdown.d.ts +52 -0
- package/dist/utils/parse-incomplete-markdown.js +4 -4
- package/package.json +15 -1
package/README.md
CHANGED
|
@@ -462,6 +462,45 @@ This heading will use a custom component!`;
|
|
|
462
462
|
/>
|
|
463
463
|
```
|
|
464
464
|
|
|
465
|
+
## 📦 Bundle Optimization
|
|
466
|
+
|
|
467
|
+
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:
|
|
468
|
+
|
|
469
|
+
### Enabling Heavy Components
|
|
470
|
+
|
|
471
|
+
```svelte
|
|
472
|
+
<script>
|
|
473
|
+
import { Streamdown } from 'svelte-streamdown';
|
|
474
|
+
// Import only the components you need
|
|
475
|
+
import Code from 'svelte-streamdown/code'; // Shiki syntax highlighting
|
|
476
|
+
import Mermaid from 'svelte-streamdown/mermaid'; // Mermaid diagrams
|
|
477
|
+
import Math from 'svelte-streamdown/math'; // KaTeX math rendering
|
|
478
|
+
</script>
|
|
479
|
+
|
|
480
|
+
<Streamdown
|
|
481
|
+
{content}
|
|
482
|
+
components={{ code: Code, mermaid: Mermaid, math: Math }}
|
|
483
|
+
/>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Component Dependencies
|
|
487
|
+
|
|
488
|
+
| Component | Import Path | Dependency | Size Impact |
|
|
489
|
+
|-----------|-------------|------------|-------------|
|
|
490
|
+
| `Code` | `svelte-streamdown/code` | Shiki | ~2MB (languages + themes) |
|
|
491
|
+
| `Mermaid` | `svelte-streamdown/mermaid` | Mermaid.js | ~1.5MB |
|
|
492
|
+
| `Math` | `svelte-streamdown/math` | KaTeX | ~300KB |
|
|
493
|
+
|
|
494
|
+
> [!TIP]
|
|
495
|
+
> 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.
|
|
496
|
+
|
|
497
|
+
### Fallback Behavior
|
|
498
|
+
|
|
499
|
+
When a heavy component is not provided:
|
|
500
|
+
- **Code blocks**: Render as plain `<pre><code>` without syntax highlighting
|
|
501
|
+
- **Mermaid**: Renders the mermaid source as a code block
|
|
502
|
+
- **Math**: Renders the raw LaTeX/math text
|
|
503
|
+
|
|
465
504
|
## 📋 Props API
|
|
466
505
|
|
|
467
506
|
| Prop | Type | Default | Description |
|
|
@@ -479,7 +518,9 @@ This heading will use a custom component!`;
|
|
|
479
518
|
| `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
|
|
480
519
|
| `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
|
|
481
520
|
| `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
|
|
482
|
-
| `shikiTheme` | `
|
|
521
|
+
| `shikiTheme` | `string` | `'github-light'` | Code highlighting theme (`github-dark` or `github-light` by default, or custom theme key) |
|
|
522
|
+
| `shikiThemes` | `Record<string, ThemeRegistration>` | - | Additional themes as pre-imported objects (e.g., `{ nord: nordTheme }`) |
|
|
523
|
+
| `shikiLanguages` | `LanguageInfo[]` | - | Additional syntax highlighting languages (merged with defaults) |
|
|
483
524
|
| `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
|
|
484
525
|
| `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
|
|
485
526
|
| `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
|
|
@@ -490,6 +531,8 @@ This heading will use a custom component!`;
|
|
|
490
531
|
| `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
|
|
491
532
|
| `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 |
|
|
492
533
|
| `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
|
|
534
|
+
| `mdxComponents` | `Record<string, Component>` | `{}` | Map of MDX component names to Svelte components (e.g., `{ Card, Button }`) |
|
|
535
|
+
| `components` | `{ code?, mermaid?, math? }` | - | Optional heavy components for syntax highlighting, diagrams, and math rendering |
|
|
493
536
|
| `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
|
|
494
537
|
|
|
495
538
|
#### All Available Customizable Elements:
|
|
@@ -506,9 +549,9 @@ This heading will use a custom component!`;
|
|
|
506
549
|
|
|
507
550
|
**Special Content**: `blockquote`, `hr`, `alert`, `mermaid`, `math`, `footnoteRef`, `inlineCitation`
|
|
508
551
|
|
|
509
|
-
**MDX Components**:
|
|
552
|
+
**MDX Components**: Handled via a single `mdx` snippet that receives `token`, `props`, and `children`. Use `token.tagName` to differentiate between components.
|
|
510
553
|
|
|
511
|
-
**Note**: The above elements are **supported by Streamdown** and should be customized using individual props or the theme system.
|
|
554
|
+
**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.
|
|
512
555
|
|
|
513
556
|
## 🎨 Theming System
|
|
514
557
|
|
|
@@ -637,24 +680,80 @@ This is **markdown content** inside a component!
|
|
|
637
680
|
</script>
|
|
638
681
|
|
|
639
682
|
<Streamdown {content}>
|
|
640
|
-
{#snippet
|
|
641
|
-
|
|
642
|
-
<
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
683
|
+
{#snippet mdx({ token, props, children })}
|
|
684
|
+
{#if token.tagName === 'Card'}
|
|
685
|
+
<div class="rounded-lg border border-gray-200 p-4 shadow-sm">
|
|
686
|
+
<h3 class="text-xl font-bold">{props.title}</h3>
|
|
687
|
+
<p class="text-gray-600">Count: {props.count}</p>
|
|
688
|
+
<div class="mt-2">
|
|
689
|
+
{@render children()}
|
|
690
|
+
</div>
|
|
646
691
|
</div>
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
{
|
|
653
|
-
|
|
692
|
+
{:else if token.tagName === 'Button'}
|
|
693
|
+
<button class="rounded px-4 py-2 {props.active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
|
|
694
|
+
{props.label}
|
|
695
|
+
</button>
|
|
696
|
+
{:else}
|
|
697
|
+
{@render children()}
|
|
698
|
+
{/if}
|
|
654
699
|
{/snippet}
|
|
655
700
|
</Streamdown>
|
|
656
701
|
```
|
|
657
702
|
|
|
703
|
+
### Alternative: Using Svelte Components Directly
|
|
704
|
+
|
|
705
|
+
Instead of using the `mdx` snippet with conditional logic, you can pass Svelte components directly using the `mdxComponents` prop:
|
|
706
|
+
|
|
707
|
+
```svelte
|
|
708
|
+
<script>
|
|
709
|
+
import { Streamdown } from 'svelte-streamdown';
|
|
710
|
+
import Card from './Card.svelte';
|
|
711
|
+
import Button from './Button.svelte';
|
|
712
|
+
|
|
713
|
+
let content = `
|
|
714
|
+
# Using MDX Components
|
|
715
|
+
|
|
716
|
+
<Card title="Hello" count={42}>
|
|
717
|
+
This is **markdown content** inside a component!
|
|
718
|
+
</Card>
|
|
719
|
+
|
|
720
|
+
<Button label="Click me" active={true} />
|
|
721
|
+
`;
|
|
722
|
+
</script>
|
|
723
|
+
|
|
724
|
+
<Streamdown {content} mdxComponents={{ Card, Button }} />
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
**Your Svelte components** (`Card.svelte`, `Button.svelte`) should accept props and a `children` snippet:
|
|
728
|
+
|
|
729
|
+
```svelte
|
|
730
|
+
<!-- Card.svelte -->
|
|
731
|
+
<script>
|
|
732
|
+
let { title, count, children } = $props();
|
|
733
|
+
</script>
|
|
734
|
+
|
|
735
|
+
<div class="rounded-lg border border-gray-200 p-4 shadow-sm">
|
|
736
|
+
<h3 class="text-xl font-bold">{title}</h3>
|
|
737
|
+
<p class="text-gray-600">Count: {count}</p>
|
|
738
|
+
<div class="mt-2">
|
|
739
|
+
{@render children()}
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
```svelte
|
|
745
|
+
<!-- Button.svelte -->
|
|
746
|
+
<script>
|
|
747
|
+
let { label, active } = $props();
|
|
748
|
+
</script>
|
|
749
|
+
|
|
750
|
+
<button class="rounded px-4 py-2 {active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
|
|
751
|
+
{label}
|
|
752
|
+
</button>
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
This approach is cleaner when you have standalone component files, while the `mdx` snippet approach is better for inline component definitions or when you need shared logic across components.
|
|
756
|
+
|
|
658
757
|
### Supported Syntax
|
|
659
758
|
|
|
660
759
|
**Self-closing components:**
|
|
@@ -689,7 +788,7 @@ MDX components support three attribute value types:
|
|
|
689
788
|
|
|
690
789
|
MDX components are streaming-safe. Incomplete components are automatically handled during AI streaming:
|
|
691
790
|
|
|
692
|
-
- Incomplete tags like `<Component attr`
|
|
791
|
+
- Incomplete tags like `<Component attr` not rendered to prevent runtime errors
|
|
693
792
|
- Unclosed components like `<Card>content` are auto-closed with `</Card>`
|
|
694
793
|
- Malformed attributes are escaped to prevent rendering errors
|
|
695
794
|
|
|
@@ -697,21 +796,31 @@ This ensures your UI remains stable even when receiving partial markdown from st
|
|
|
697
796
|
|
|
698
797
|
### Component Props
|
|
699
798
|
|
|
700
|
-
|
|
701
|
-
- `token`: The full MdxToken with `tagName`, `attributes`, etc.
|
|
799
|
+
The `mdx` snippet receives three parameters:
|
|
800
|
+
- `token`: The full MdxToken with `tagName`, `attributes`, `selfClosing`, etc.
|
|
801
|
+
- `props`: Object containing all parsed attributes (e.g., `props.title`, `props.count`)
|
|
702
802
|
- `children`: Snippet containing parsed markdown content
|
|
703
|
-
- **All attributes spread directly**: Access attributes by name (e.g., `title`, `count`, `active`)
|
|
704
803
|
|
|
705
|
-
|
|
804
|
+
Use `token.tagName` to determine which component is being rendered:
|
|
805
|
+
|
|
706
806
|
```svelte
|
|
707
807
|
<!-- Markdown: <Card title="Hello" count={5}>Content</Card> -->
|
|
708
808
|
<Streamdown {content}>
|
|
709
|
-
{#snippet
|
|
710
|
-
|
|
711
|
-
<
|
|
712
|
-
|
|
809
|
+
{#snippet mdx({ token, props, children })}
|
|
810
|
+
{#if token.tagName === 'Card'}
|
|
811
|
+
<div>
|
|
812
|
+
<h3>{props.title}</h3>
|
|
813
|
+
<span>Count: {props.count}</span>
|
|
814
|
+
{@render children()}
|
|
815
|
+
</div>
|
|
816
|
+
{:else if token.tagName === 'Alert'}
|
|
817
|
+
<div class="alert alert-{props.type}">
|
|
818
|
+
{@render children()}
|
|
819
|
+
</div>
|
|
820
|
+
{:else}
|
|
821
|
+
<!-- Fallback for unknown components -->
|
|
713
822
|
{@render children()}
|
|
714
|
-
|
|
823
|
+
{/if}
|
|
715
824
|
{/snippet}
|
|
716
825
|
</Streamdown>
|
|
717
826
|
```
|
package/dist/Block.svelte
CHANGED
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
import { getContext } from 'svelte';
|
|
8
8
|
|
|
9
9
|
let {
|
|
10
|
-
block
|
|
10
|
+
block,
|
|
11
|
+
static: isStatic = false
|
|
11
12
|
}: {
|
|
12
13
|
block: string;
|
|
14
|
+
static?: boolean;
|
|
13
15
|
} = $props();
|
|
14
16
|
|
|
15
17
|
const streamdown = useStreamdown();
|
|
16
|
-
const tokens = $derived(
|
|
18
|
+
const tokens = $derived(
|
|
19
|
+
lex(isStatic ? block : parseIncompleteMarkdown(block.trim()), streamdown.extensions)
|
|
20
|
+
);
|
|
17
21
|
const insidePopover = getContext('POPOVER');
|
|
18
22
|
</script>
|
|
19
23
|
|
|
@@ -24,8 +28,8 @@
|
|
|
24
28
|
{@const isTextOnlyNode = children.length === 0}
|
|
25
29
|
<Element {token}>
|
|
26
30
|
{#if isTextOnlyNode}
|
|
27
|
-
{#if streamdown.animation.enabled && !insidePopover}
|
|
28
|
-
<AnimatedText text={'text' in token ? token.text : ''} />
|
|
31
|
+
{#if streamdown.animation.enabled && !insidePopover && !isStatic}
|
|
32
|
+
<AnimatedText text={'text' in token ? token.text || '' : ''} />
|
|
29
33
|
{:else}
|
|
30
34
|
{'text' in token ? token.text : ''}
|
|
31
35
|
{/if}
|
package/dist/Block.svelte.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { save } from '../utils/save.js';
|
|
4
4
|
import { useCopy } from '../utils/copy.svelte.js';
|
|
5
5
|
import { HighlighterManager, languageExtensionMap } from '../utils/hightlighter.svelte.js';
|
|
6
|
+
import { bundledLanguagesInfo } from '../utils/bundledLanguages.js';
|
|
6
7
|
import type { Tokens } from 'marked';
|
|
7
8
|
import { type ThemedToken } from 'shiki';
|
|
8
9
|
import { untrack } from 'svelte';
|
|
@@ -17,7 +18,11 @@
|
|
|
17
18
|
} = $props();
|
|
18
19
|
|
|
19
20
|
const streamdown = useStreamdown();
|
|
20
|
-
const highlighter = HighlighterManager.create(
|
|
21
|
+
const highlighter = HighlighterManager.create(
|
|
22
|
+
bundledLanguagesInfo,
|
|
23
|
+
streamdown.shikiThemes,
|
|
24
|
+
streamdown.shikiLanguages
|
|
25
|
+
);
|
|
21
26
|
|
|
22
27
|
const copy = useCopy({
|
|
23
28
|
get content() {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import Link from './Link.svelte';
|
|
4
|
-
import Code from './Code.svelte';
|
|
5
4
|
import Image from './Image.svelte';
|
|
6
|
-
import Mermaid from './Mermaid.svelte';
|
|
7
|
-
import Math from './Math.svelte';
|
|
8
5
|
import Alert from './Alert.svelte';
|
|
9
6
|
import type { StreamdownToken } from '../marked/index.js';
|
|
10
7
|
import Slot from './Slot.svelte';
|
|
@@ -12,9 +9,16 @@
|
|
|
12
9
|
import FootnoteRef from './FootnoteRef.svelte';
|
|
13
10
|
import Citation from './Citation.svelte';
|
|
14
11
|
import TableDownload from './TableDownload.svelte';
|
|
12
|
+
// Import fallback components
|
|
13
|
+
import { CodeFallback, MermaidFallback, MathFallback } from './fallbacks/index.js';
|
|
15
14
|
let { token, children }: { token: StreamdownToken; children: Snippet } = $props();
|
|
16
15
|
const streamdown = useStreamdown();
|
|
17
16
|
|
|
17
|
+
// Use provided components or fallback to lightweight versions
|
|
18
|
+
const CodeComponent = $derived(streamdown.components?.code ?? CodeFallback);
|
|
19
|
+
const MermaidComponent = $derived(streamdown.components?.mermaid ?? MermaidFallback);
|
|
20
|
+
const MathComponent = $derived(streamdown.components?.math ?? MathFallback);
|
|
21
|
+
|
|
18
22
|
// Only apply animation on block level elements. Leaves text elements to be animated by their text children.
|
|
19
23
|
const style = $derived(streamdown.isMounted ? streamdown.animationBlockStyle : '');
|
|
20
24
|
const id = $props.id();
|
|
@@ -68,11 +72,11 @@
|
|
|
68
72
|
</Slot>
|
|
69
73
|
{:else if token.type === 'code' && token.lang === 'mermaid'}
|
|
70
74
|
<Slot props={{ children, token }} render={streamdown.snippets.code}>
|
|
71
|
-
<
|
|
75
|
+
<MermaidComponent {id} {token} />
|
|
72
76
|
</Slot>
|
|
73
77
|
{:else if token.type === 'code'}
|
|
74
78
|
<Slot props={{ children, token }} render={streamdown.snippets.code}>
|
|
75
|
-
<
|
|
79
|
+
<CodeComponent {id} {token} />
|
|
76
80
|
</Slot>
|
|
77
81
|
{:else if token.type === 'codespan'}
|
|
78
82
|
<Slot props={{ children, token }} render={streamdown.snippets.codespan}>
|
|
@@ -240,7 +244,7 @@
|
|
|
240
244
|
}}
|
|
241
245
|
render={streamdown.snippets.math}
|
|
242
246
|
>
|
|
243
|
-
<
|
|
247
|
+
<MathComponent {id} {token} />
|
|
244
248
|
</Slot>
|
|
245
249
|
{:else if token.type === 'alert'}
|
|
246
250
|
<Alert {id} {token} {children} />
|
|
@@ -299,16 +303,13 @@
|
|
|
299
303
|
{@html content}
|
|
300
304
|
{/if}
|
|
301
305
|
{:else if token.type === 'mdx'}
|
|
302
|
-
{
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
>
|
|
306
|
+
{@const Component = streamdown.mdxComponents?.[token.tagName]}
|
|
307
|
+
{#if Component}
|
|
308
|
+
<Component {token} {children} props={token.attributes} />
|
|
309
|
+
{:else}
|
|
310
|
+
<Slot props={{ token, children, props: token.attributes }} render={streamdown.snippets.mdx}>
|
|
307
311
|
{@render children()}
|
|
308
312
|
</Slot>
|
|
309
|
-
{:else}
|
|
310
|
-
<!-- Fallback if no snippet provided for this component -->
|
|
311
|
-
{@render children()}
|
|
312
313
|
{/if}
|
|
313
314
|
{:else}
|
|
314
315
|
<!-- For tokens we don't handle specifically, it may certainely be a custom extension to to the children props to handle -->
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { on } from 'svelte/events';
|
|
7
7
|
import { usePanzoom } from '../utils/panzoom.svelte';
|
|
8
8
|
import { fitViewIcon, fullscreenIcon, zoomInIcon, zoomOutIcon } from './icons.js';
|
|
9
|
+
import MermaidDownload from './MermaidDownload.svelte';
|
|
9
10
|
|
|
10
11
|
const streamdown = useStreamdown();
|
|
11
12
|
|
|
@@ -254,6 +255,7 @@
|
|
|
254
255
|
>
|
|
255
256
|
{@render (streamdown.icons?.fullscreen || fullscreenIcon)()}
|
|
256
257
|
</button>
|
|
258
|
+
<MermaidDownload {id} />
|
|
257
259
|
</div>
|
|
258
260
|
{/if}
|
|
259
261
|
<svg {@attach panzoom.attach} data-mermaid-svg></svg>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useStreamdown } from '../context.svelte.js';
|
|
3
|
+
import { scale } from 'svelte/transition';
|
|
4
|
+
import { downloadIcon } from './icons.js';
|
|
5
|
+
import { Popover } from './popover.svelte.js';
|
|
6
|
+
import { useClickOutside } from '../utils/useClickOutside.svelte.js';
|
|
7
|
+
import { useKeyDown } from '../utils/useKeyDown.svelte.js';
|
|
8
|
+
import { save } from '../utils/save.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
id
|
|
12
|
+
}: {
|
|
13
|
+
id: string;
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
const streamdown = useStreamdown();
|
|
17
|
+
const popover = new Popover();
|
|
18
|
+
|
|
19
|
+
useKeyDown({
|
|
20
|
+
keys: ['Escape'],
|
|
21
|
+
get isActive() {
|
|
22
|
+
return popover.isOpen;
|
|
23
|
+
},
|
|
24
|
+
callback: () => {
|
|
25
|
+
popover.isOpen = false;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const clickOutside = useClickOutside({
|
|
30
|
+
get isActive() {
|
|
31
|
+
return popover.isOpen;
|
|
32
|
+
},
|
|
33
|
+
callback: () => {
|
|
34
|
+
popover.isOpen = false;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const getSvgElement = (): SVGSVGElement | null => {
|
|
39
|
+
const container = document.querySelector(`[data-streamdown-mermaid="${id}"]`);
|
|
40
|
+
if (!container) return null;
|
|
41
|
+
|
|
42
|
+
const svgContainer = container.querySelector('[data-mermaid-svg]');
|
|
43
|
+
if (!svgContainer) return null;
|
|
44
|
+
|
|
45
|
+
// The actual SVG is rendered inside the data-mermaid-svg container
|
|
46
|
+
const svg = svgContainer.querySelector('svg');
|
|
47
|
+
return svg;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const downloadSvg = () => {
|
|
51
|
+
const svg = getSvgElement();
|
|
52
|
+
if (!svg) return;
|
|
53
|
+
|
|
54
|
+
// Clone the SVG to avoid modifying the original
|
|
55
|
+
const clonedSvg = svg.cloneNode(true) as SVGSVGElement;
|
|
56
|
+
|
|
57
|
+
// Ensure the SVG has proper xmlns
|
|
58
|
+
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
59
|
+
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
60
|
+
|
|
61
|
+
// Get computed styles and inline them for standalone SVG
|
|
62
|
+
const styles = getComputedStyle(svg);
|
|
63
|
+
if (!clonedSvg.getAttribute('width')) {
|
|
64
|
+
clonedSvg.setAttribute('width', styles.width);
|
|
65
|
+
}
|
|
66
|
+
if (!clonedSvg.getAttribute('height')) {
|
|
67
|
+
clonedSvg.setAttribute('height', styles.height);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const svgString = new XMLSerializer().serializeToString(clonedSvg);
|
|
71
|
+
save('mermaid-diagram.svg', svgString, 'image/svg+xml');
|
|
72
|
+
popover.isOpen = false;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const downloadPng = async () => {
|
|
76
|
+
const svg = getSvgElement();
|
|
77
|
+
if (!svg) return;
|
|
78
|
+
|
|
79
|
+
// Clone the SVG to avoid modifying the original
|
|
80
|
+
const clonedSvg = svg.cloneNode(true) as SVGSVGElement;
|
|
81
|
+
|
|
82
|
+
// Ensure the SVG has proper xmlns
|
|
83
|
+
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
84
|
+
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
85
|
+
|
|
86
|
+
// Get dimensions
|
|
87
|
+
const bbox = svg.getBBox();
|
|
88
|
+
const styles = getComputedStyle(svg);
|
|
89
|
+
const width = parseFloat(styles.width) || bbox.width || 800;
|
|
90
|
+
const height = parseFloat(styles.height) || bbox.height || 600;
|
|
91
|
+
|
|
92
|
+
// Set explicit dimensions on the cloned SVG
|
|
93
|
+
clonedSvg.setAttribute('width', String(width));
|
|
94
|
+
clonedSvg.setAttribute('height', String(height));
|
|
95
|
+
|
|
96
|
+
// Serialize SVG to string
|
|
97
|
+
const svgString = new XMLSerializer().serializeToString(clonedSvg);
|
|
98
|
+
|
|
99
|
+
// Use data URL instead of blob URL to avoid tainted canvas issues
|
|
100
|
+
const svgDataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));
|
|
101
|
+
|
|
102
|
+
// Create an image to load the SVG
|
|
103
|
+
const img = new Image();
|
|
104
|
+
|
|
105
|
+
img.onload = () => {
|
|
106
|
+
// Create a canvas with 2x scale for better quality
|
|
107
|
+
const scale = 2;
|
|
108
|
+
const canvas = document.createElement('canvas');
|
|
109
|
+
canvas.width = width * scale;
|
|
110
|
+
canvas.height = height * scale;
|
|
111
|
+
|
|
112
|
+
const ctx = canvas.getContext('2d');
|
|
113
|
+
if (!ctx) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fill with white background (optional, remove for transparent)
|
|
118
|
+
ctx.fillStyle = '#ffffff';
|
|
119
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
120
|
+
|
|
121
|
+
// Scale and draw the image
|
|
122
|
+
ctx.scale(scale, scale);
|
|
123
|
+
ctx.drawImage(img, 0, 0);
|
|
124
|
+
|
|
125
|
+
// Convert to PNG and download
|
|
126
|
+
canvas.toBlob((blob) => {
|
|
127
|
+
if (blob) {
|
|
128
|
+
const url = URL.createObjectURL(blob);
|
|
129
|
+
const link = document.createElement('a');
|
|
130
|
+
link.href = url;
|
|
131
|
+
link.download = 'mermaid-diagram.png';
|
|
132
|
+
document.body.appendChild(link);
|
|
133
|
+
link.click();
|
|
134
|
+
document.body.removeChild(link);
|
|
135
|
+
URL.revokeObjectURL(url);
|
|
136
|
+
}
|
|
137
|
+
}, 'image/png');
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
img.onerror = () => {
|
|
141
|
+
console.error('Failed to load SVG for PNG conversion');
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
img.src = svgDataUrl;
|
|
145
|
+
popover.isOpen = false;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const download = (type: 'SVG' | 'PNG') => {
|
|
149
|
+
if (type === 'SVG') {
|
|
150
|
+
downloadSvg();
|
|
151
|
+
} else {
|
|
152
|
+
downloadPng();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
{#if popover.isOpen}
|
|
158
|
+
<dialog
|
|
159
|
+
id={'mermaid-download-popover'}
|
|
160
|
+
aria-modal="false"
|
|
161
|
+
transition:scale|global={{ start: 0.95, duration: 100 }}
|
|
162
|
+
{@attach clickOutside.attachment}
|
|
163
|
+
{@attach popover.popoverAttachment}
|
|
164
|
+
open
|
|
165
|
+
style:width="fit-content !important"
|
|
166
|
+
style:min-width="fit-content !important"
|
|
167
|
+
class={streamdown.theme.components.popover}
|
|
168
|
+
>
|
|
169
|
+
{#each ['PNG', 'SVG'] as type}
|
|
170
|
+
<button
|
|
171
|
+
style="width: 100%; text-align: left; justify-content: flex-start; padding: 1rem 1rem; margin: 0.2rem 0;"
|
|
172
|
+
onclick={() => download(type as 'SVG' | 'PNG')}
|
|
173
|
+
class={streamdown.theme.components.button}
|
|
174
|
+
>
|
|
175
|
+
{type}
|
|
176
|
+
</button>
|
|
177
|
+
{/each}
|
|
178
|
+
</dialog>
|
|
179
|
+
{/if}
|
|
180
|
+
|
|
181
|
+
<button
|
|
182
|
+
class={streamdown.theme.components.button}
|
|
183
|
+
onclick={(e: MouseEvent) => {
|
|
184
|
+
if (popover.isOpen) {
|
|
185
|
+
popover.isOpen = false;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
popover.reference = e.target as HTMLButtonElement;
|
|
189
|
+
popover.isOpen = true;
|
|
190
|
+
}}
|
|
191
|
+
{@attach clickOutside.attachment}
|
|
192
|
+
title="Download diagram"
|
|
193
|
+
data-panzoom-ignore
|
|
194
|
+
>
|
|
195
|
+
{@render (streamdown.icons?.download || downloadIcon)()}
|
|
196
|
+
</button>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useStreamdown } from '../../context.svelte.js';
|
|
3
|
+
import type { Tokens } from 'marked';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
token,
|
|
7
|
+
id
|
|
8
|
+
}: {
|
|
9
|
+
token: Tokens.Code;
|
|
10
|
+
id: string;
|
|
11
|
+
} = $props();
|
|
12
|
+
|
|
13
|
+
const streamdown = useStreamdown();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
data-streamdown-code={id}
|
|
18
|
+
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
19
|
+
class={streamdown.theme.code.base}
|
|
20
|
+
>
|
|
21
|
+
<div class={streamdown.theme.code.header}>
|
|
22
|
+
<span class={streamdown.theme.code.language}>{token.lang}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
25
|
+
<pre class={streamdown.theme.code.pre}><code
|
|
26
|
+
>{#each token.text.split('\n') as line}<span class={streamdown.theme.code.line}
|
|
27
|
+
><span style={streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
28
|
+
>{line.trim().length > 0 ? line : '\u200B'}</span
|
|
29
|
+
></span
|
|
30
|
+
>{/each}</code
|
|
31
|
+
></pre>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Tokens } from 'marked';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
token: Tokens.Code;
|
|
4
|
+
id: string;
|
|
5
|
+
};
|
|
6
|
+
declare const CodeFallback: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type CodeFallback = ReturnType<typeof CodeFallback>;
|
|
8
|
+
export default CodeFallback;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useStreamdown } from '../../context.svelte.js';
|
|
3
|
+
import type { MathToken } from '../../marked/index.js';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
token,
|
|
7
|
+
id
|
|
8
|
+
}: {
|
|
9
|
+
token: MathToken;
|
|
10
|
+
id: string;
|
|
11
|
+
} = $props();
|
|
12
|
+
|
|
13
|
+
const streamdown = useStreamdown();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
{#if token.isInline}
|
|
17
|
+
<span
|
|
18
|
+
data-streamdown-inline-math={id}
|
|
19
|
+
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
20
|
+
class={streamdown.theme.math.inline}
|
|
21
|
+
>
|
|
22
|
+
<code>{token.text}</code>
|
|
23
|
+
</span>
|
|
24
|
+
{:else}
|
|
25
|
+
<div
|
|
26
|
+
data-streamdown-block-math={id}
|
|
27
|
+
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
28
|
+
style:height="fit-content"
|
|
29
|
+
style:width="100%"
|
|
30
|
+
>
|
|
31
|
+
<div class="overflow-x-auto">
|
|
32
|
+
<pre class={streamdown.theme.math.block}><code>{token.text}</code></pre>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MathToken } from '../../marked/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
token: MathToken;
|
|
4
|
+
id: string;
|
|
5
|
+
};
|
|
6
|
+
declare const MathFallback: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type MathFallback = ReturnType<typeof MathFallback>;
|
|
8
|
+
export default MathFallback;
|