svelte-streamdown 0.0.1 → 1.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/LICENSE +21 -0
- package/README.md +511 -31
- package/dist/Elements/Code.svelte +27 -17
- package/dist/Elements/Element.svelte +11 -3
- package/dist/Elements/Image.svelte +2 -7
- package/dist/Elements/Math.svelte +29 -47
- package/dist/Elements/Mermaid.svelte +207 -5
- package/dist/Elements/Table.svelte +2 -2
- package/dist/Elements/element.d.ts +1 -1
- package/dist/Streamdown.d.ts +8 -35
- package/dist/Streamdown.svelte +22 -3
- package/dist/hightlighter.svelte.d.ts +18 -12
- package/dist/hightlighter.svelte.js +91 -65
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/theme.d.ts +127 -14
- package/dist/theme.js +139 -22
- package/dist/utils/panzoom.svelte.d.ts +36 -0
- package/dist/utils/panzoom.svelte.js +483 -0
- package/package.json +3 -2
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
import Em from './Em.svelte';
|
|
42
42
|
import Del from './Del.svelte';
|
|
43
43
|
import Alert from './Alert.svelte';
|
|
44
|
+
import { useStreamdown } from '../Streamdown.svelte';
|
|
45
|
+
import { type ElementProps } from './element.js';
|
|
46
|
+
|
|
47
|
+
const streamdown = useStreamdown();
|
|
44
48
|
</script>
|
|
45
49
|
|
|
46
50
|
{#if type === 'a'}
|
|
@@ -110,13 +114,17 @@
|
|
|
110
114
|
<br />
|
|
111
115
|
{:else if type === 'em'}
|
|
112
116
|
<Em {props} {className} {children} {node} />
|
|
113
|
-
{:else if type === 'ins'}
|
|
114
|
-
<!-- <Ins {props} {children} {node} /> -->
|
|
115
117
|
{:else if type === 'del'}
|
|
116
118
|
<Del {props} {className} {children} {node} />
|
|
117
119
|
{:else if type === 'alert'}
|
|
118
120
|
<Alert {props} {className} {children} {node} />
|
|
119
121
|
{:else}
|
|
122
|
+
{@const snippet = streamdown.customElements?.[type] as Snippet<[ElementProps]> | undefined}
|
|
123
|
+
{#if snippet}
|
|
124
|
+
{@render snippet({ props, className, children, node })}
|
|
125
|
+
{:else}
|
|
126
|
+
{@render children?.()}
|
|
127
|
+
{/if}
|
|
128
|
+
|
|
120
129
|
<!-- Fallback for unsupported elements -->
|
|
121
|
-
{@render children?.()}
|
|
122
130
|
{/if}
|
|
@@ -27,13 +27,8 @@
|
|
|
27
27
|
}}
|
|
28
28
|
render={streamdown.snippets.img}
|
|
29
29
|
>
|
|
30
|
-
<div class={streamdown.theme.img.
|
|
31
|
-
<img
|
|
32
|
-
src={transformedUrl}
|
|
33
|
-
{alt}
|
|
34
|
-
{...props}
|
|
35
|
-
class={clsx(streamdown.theme.img.base, className)}
|
|
36
|
-
/>
|
|
30
|
+
<div class={clsx(streamdown.theme.img.base, className)} {...props}>
|
|
31
|
+
<img class={streamdown.theme.img.image} src={transformedUrl} {alt} />
|
|
37
32
|
</div>
|
|
38
33
|
</Slot>
|
|
39
34
|
{:else}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
3
|
import { useStreamdown } from '../Streamdown.svelte';
|
|
4
4
|
import { clsx } from 'clsx';
|
|
5
5
|
import type { ElementProps } from './element.js';
|
|
@@ -8,35 +8,27 @@
|
|
|
8
8
|
import 'katex/dist/katex.min.css';
|
|
9
9
|
const streamdown = useStreamdown();
|
|
10
10
|
const { children, node, className, props }: ElementProps = $props();
|
|
11
|
-
|
|
12
|
-
const isInline = className?.includes('inline') ?? false;
|
|
13
|
-
let katex = $state<typeof import('katex').renderToString | null>(null);
|
|
14
|
-
onMount(async () => {
|
|
15
|
-
katex = (await import('katex')).renderToString;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const renderMath = async (code: string, element: HTMLElement) => {
|
|
19
|
-
console.log({ code, node });
|
|
11
|
+
import katex from 'katex';
|
|
12
|
+
const isInline = className?.some?.((c: string) => c.includes('inline')) ?? false;
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
let inner = $state<HTMLElement | null>(null);
|
|
15
|
+
const html = $derived.by(() => {
|
|
16
|
+
const config: KatexOptions = {
|
|
17
|
+
output: 'html',
|
|
18
|
+
displayMode: !isInline,
|
|
19
|
+
...(typeof streamdown.katexConfig === 'function'
|
|
20
|
+
? streamdown.katexConfig(isInline)
|
|
21
|
+
: streamdown.katexConfig || {})
|
|
22
|
+
};
|
|
23
|
+
const code = ((node.children[0] as any)?.value as string) ?? '';
|
|
22
24
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const config: KatexOptions = {
|
|
29
|
-
output: 'html',
|
|
30
|
-
displayMode: !isInline,
|
|
31
|
-
...(customConfig || {})
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const html = katex(code, config);
|
|
35
|
-
element.innerHTML = html.toString();
|
|
36
|
-
} catch (err) {
|
|
37
|
-
// Do nothing
|
|
25
|
+
return katex.renderToString(code, config);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return untrack(() => {
|
|
28
|
+
return inner?.innerHTML || '';
|
|
29
|
+
});
|
|
38
30
|
}
|
|
39
|
-
};
|
|
31
|
+
});
|
|
40
32
|
</script>
|
|
41
33
|
|
|
42
34
|
<Slot
|
|
@@ -47,27 +39,17 @@
|
|
|
47
39
|
}}
|
|
48
40
|
render={streamdown.snippets.math}
|
|
49
41
|
>
|
|
50
|
-
{#if
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{:else}
|
|
60
|
-
<div class="h-fit w-full">
|
|
61
|
-
<div class="overflow-x-auto">
|
|
62
|
-
<div
|
|
63
|
-
{...props}
|
|
64
|
-
class={clsx(streamdown.theme.math.base, className)}
|
|
65
|
-
{@attach (node) => renderMath(code, node)}
|
|
66
|
-
></div>
|
|
42
|
+
{#if isInline}
|
|
43
|
+
<span bind:this={inner} {...props} class={clsx(streamdown.theme.inlineMath.base, className)}>
|
|
44
|
+
{@html html}
|
|
45
|
+
</span>
|
|
46
|
+
{:else}
|
|
47
|
+
<div class="h-fit w-full">
|
|
48
|
+
<div class="overflow-x-auto">
|
|
49
|
+
<div bind:this={inner} {...props} class={clsx(streamdown.theme.math.base, className)}>
|
|
50
|
+
{@html html}
|
|
67
51
|
</div>
|
|
68
52
|
</div>
|
|
69
|
-
|
|
70
|
-
{:else}
|
|
71
|
-
<div {...props} class={clsx(streamdown.theme.math.base, className)}></div>
|
|
53
|
+
</div>
|
|
72
54
|
{/if}
|
|
73
55
|
</Slot>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
3
|
import { useStreamdown } from '../Streamdown.svelte';
|
|
4
4
|
import { clsx } from 'clsx';
|
|
5
5
|
import type { ElementProps } from './element.js';
|
|
6
6
|
import Slot from './Slot.svelte';
|
|
7
7
|
import type { MermaidConfig } from 'mermaid';
|
|
8
|
+
import { on } from 'svelte/events';
|
|
9
|
+
import { usePanzoom } from '../utils/panzoom.svelte';
|
|
8
10
|
|
|
9
11
|
const streamdown = useStreamdown();
|
|
10
12
|
|
|
@@ -16,6 +18,45 @@
|
|
|
16
18
|
mermaid = (await import('mermaid')).default;
|
|
17
19
|
});
|
|
18
20
|
|
|
21
|
+
const useIsInsideForMoreThanAQuarterSecond = () => {
|
|
22
|
+
let isInside = $state(false);
|
|
23
|
+
let timeout: number | undefined = undefined;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
get isInside() {
|
|
27
|
+
return isInside;
|
|
28
|
+
},
|
|
29
|
+
attach: (node: HTMLElement) => {
|
|
30
|
+
const off1 = on(node, 'mouseenter', () => {
|
|
31
|
+
timeout = setTimeout(() => {
|
|
32
|
+
isInside = true;
|
|
33
|
+
}, 800);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const off2 = on(node, 'mouseleave', () => {
|
|
37
|
+
isInside = false;
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
off1();
|
|
43
|
+
off2();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const insider = useIsInsideForMoreThanAQuarterSecond();
|
|
50
|
+
|
|
51
|
+
const panzoom = usePanzoom({
|
|
52
|
+
minZoom: 0.5,
|
|
53
|
+
maxZoom: 4,
|
|
54
|
+
zoomSpeed: 1,
|
|
55
|
+
get activateMouseWheel() {
|
|
56
|
+
return insider.isInside;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
19
60
|
const renderMermaid = async (code: string, element: HTMLElement) => {
|
|
20
61
|
try {
|
|
21
62
|
// Default configuration
|
|
@@ -25,6 +66,7 @@
|
|
|
25
66
|
securityLevel: 'strict',
|
|
26
67
|
fontFamily: 'monospace',
|
|
27
68
|
suppressErrorRendering: true,
|
|
69
|
+
|
|
28
70
|
flowchart: {
|
|
29
71
|
useMaxWidth: true,
|
|
30
72
|
htmlLabels: true,
|
|
@@ -50,10 +92,19 @@
|
|
|
50
92
|
const uniqueId = `mermaid-${Math.abs(chartHash)}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
51
93
|
|
|
52
94
|
// Render the diagram
|
|
53
|
-
const { svg } = await mermaid.render(uniqueId, code);
|
|
95
|
+
const { svg: svgString } = await mermaid.render(uniqueId, code);
|
|
96
|
+
const svg = new DOMParser().parseFromString(svgString, 'image/svg+xml').documentElement;
|
|
97
|
+
|
|
98
|
+
const svgTarget = element.querySelector('[data-mermaid-svg]')!;
|
|
99
|
+
svg.id = uniqueId;
|
|
100
|
+
Array.from(svg.attributes).forEach((attribute) => {
|
|
101
|
+
svgTarget.setAttribute(attribute.name, attribute.value);
|
|
102
|
+
});
|
|
103
|
+
svgTarget.innerHTML = svg.innerHTML;
|
|
104
|
+
// After rendering, fit the SVG within its parent container
|
|
54
105
|
|
|
55
|
-
|
|
56
|
-
|
|
106
|
+
panzoom.zoomToFit();
|
|
107
|
+
panzoom.zoomToFit();
|
|
57
108
|
} catch (err) {
|
|
58
109
|
// Do nothing
|
|
59
110
|
}
|
|
@@ -73,8 +124,159 @@
|
|
|
73
124
|
{...props}
|
|
74
125
|
class={clsx(streamdown.theme.mermaid.base, className)}
|
|
75
126
|
{@attach (node) => renderMermaid(code, node)}
|
|
76
|
-
|
|
127
|
+
{@attach insider.attach}
|
|
128
|
+
data-expanded={'false'}
|
|
129
|
+
>
|
|
130
|
+
<div class={streamdown.theme.mermaid.buttons}>
|
|
131
|
+
<button
|
|
132
|
+
class={streamdown.theme.mermaid.button}
|
|
133
|
+
aria-label="Zoom to fit"
|
|
134
|
+
onclick={() => panzoom.zoomToFit()}
|
|
135
|
+
data-panzoom-ignore
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
class={streamdown.theme.mermaid.icon}
|
|
139
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
140
|
+
viewBox="0 0 24 24"
|
|
141
|
+
fill="none"
|
|
142
|
+
stroke="currentColor"
|
|
143
|
+
stroke-width="2"
|
|
144
|
+
stroke-linecap="round"
|
|
145
|
+
stroke-linejoin="round"
|
|
146
|
+
><path d="M3 7V5a2 2 0 0 1 2-2h2" /><path d="M17 3h2a2 2 0 0 1 2 2v2" /><path
|
|
147
|
+
d="M21 17v2a2 2 0 0 1-2 2h-2"
|
|
148
|
+
/><path d="M7 21H5a2 2 0 0 1-2-2v-2" /><rect
|
|
149
|
+
width="10"
|
|
150
|
+
height="8"
|
|
151
|
+
x="7"
|
|
152
|
+
y="8"
|
|
153
|
+
rx="1"
|
|
154
|
+
/></svg
|
|
155
|
+
>
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
class={streamdown.theme.mermaid.button}
|
|
159
|
+
aria-label="Zoom in"
|
|
160
|
+
onclick={() => panzoom.zoomIn()}
|
|
161
|
+
data-panzoom-ignore
|
|
162
|
+
>
|
|
163
|
+
<svg
|
|
164
|
+
class={streamdown.theme.mermaid.icon}
|
|
165
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
166
|
+
viewBox="0 0 24 24"
|
|
167
|
+
fill="none"
|
|
168
|
+
stroke="currentColor"
|
|
169
|
+
stroke-width="2"
|
|
170
|
+
stroke-linecap="round"
|
|
171
|
+
stroke-linejoin="round"
|
|
172
|
+
><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line
|
|
173
|
+
x1="11"
|
|
174
|
+
x2="11"
|
|
175
|
+
y1="8"
|
|
176
|
+
y2="14"
|
|
177
|
+
/><line x1="8" x2="14" y1="11" y2="11" /></svg
|
|
178
|
+
>
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
class={streamdown.theme.mermaid.button}
|
|
182
|
+
aria-label="Zoom out"
|
|
183
|
+
onclick={() => panzoom.zoomOut()}
|
|
184
|
+
data-panzoom-ignore
|
|
185
|
+
><svg
|
|
186
|
+
class={streamdown.theme.mermaid.icon}
|
|
187
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
188
|
+
viewBox="0 0 24 24"
|
|
189
|
+
fill="none"
|
|
190
|
+
stroke="currentColor"
|
|
191
|
+
stroke-width="2"
|
|
192
|
+
stroke-linecap="round"
|
|
193
|
+
stroke-linejoin="round"
|
|
194
|
+
><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line
|
|
195
|
+
x1="8"
|
|
196
|
+
x2="14"
|
|
197
|
+
y1="11"
|
|
198
|
+
y2="11"
|
|
199
|
+
/></svg
|
|
200
|
+
></button
|
|
201
|
+
>
|
|
202
|
+
<button
|
|
203
|
+
class={streamdown.theme.mermaid.button}
|
|
204
|
+
aria-label="Toggle expand"
|
|
205
|
+
onclick={() => panzoom.toggleExpand()}
|
|
206
|
+
data-panzoom-ignore
|
|
207
|
+
>
|
|
208
|
+
<svg
|
|
209
|
+
class={streamdown.theme.mermaid.icon}
|
|
210
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
211
|
+
viewBox="0 0 24 24"
|
|
212
|
+
fill="none"
|
|
213
|
+
stroke="currentColor"
|
|
214
|
+
stroke-width="2"
|
|
215
|
+
stroke-linecap="round"
|
|
216
|
+
stroke-linejoin="round"
|
|
217
|
+
><path d="m15 15 6 6" /><path d="m15 9 6-6" /><path d="M21 16v5h-5" /><path
|
|
218
|
+
d="M21 8V3h-5"
|
|
219
|
+
/><path d="M3 16v5h5" /><path d="m3 21 6-6" /><path d="M3 8V3h5" /><path
|
|
220
|
+
d="M9 9 3 3"
|
|
221
|
+
/></svg
|
|
222
|
+
>
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
<svg {@attach panzoom.attach} data-mermaid-svg></svg>
|
|
226
|
+
</div>
|
|
77
227
|
{:else}
|
|
78
228
|
<div {...props} class={clsx(streamdown.theme.mermaid.base, className)}></div>
|
|
79
229
|
{/if}
|
|
80
230
|
</Slot>
|
|
231
|
+
|
|
232
|
+
<style>
|
|
233
|
+
/* View Transition styles for zoom effect */
|
|
234
|
+
::view-transition-old(panzoom-element),
|
|
235
|
+
::view-transition-new(panzoom-element) {
|
|
236
|
+
/* Animate both scale and opacity for smooth zoom */
|
|
237
|
+
animation-duration: 0.3s;
|
|
238
|
+
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Zoom in animation (expand) */
|
|
242
|
+
::view-transition-old(panzoom-element) {
|
|
243
|
+
animation-name: zoom-out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
::view-transition-new(panzoom-element) {
|
|
247
|
+
animation-name: zoom-in;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@keyframes zoom-out {
|
|
251
|
+
from {
|
|
252
|
+
transform: scale(1);
|
|
253
|
+
}
|
|
254
|
+
to {
|
|
255
|
+
transform: scale(0.8);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@keyframes zoom-in {
|
|
260
|
+
from {
|
|
261
|
+
transform: scale(1.2);
|
|
262
|
+
}
|
|
263
|
+
to {
|
|
264
|
+
transform: scale(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
[data-expanded='true'] {
|
|
269
|
+
::view-transition-old(panzoom-element),
|
|
270
|
+
::view-transition-new(panzoom-element) {
|
|
271
|
+
animation-duration: 0.3s;
|
|
272
|
+
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
|
|
273
|
+
}
|
|
274
|
+
position: fixed;
|
|
275
|
+
top: 16px;
|
|
276
|
+
left: 16px;
|
|
277
|
+
width: calc(100vw - 32px);
|
|
278
|
+
height: calc(100vh - 32px);
|
|
279
|
+
z-index: 2147483647;
|
|
280
|
+
margin: 0px;
|
|
281
|
+
}
|
|
282
|
+
</style>
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}}
|
|
18
18
|
render={streamdown.snippets.table}
|
|
19
19
|
>
|
|
20
|
-
<div class={clsx(streamdown.theme.table.
|
|
21
|
-
<table
|
|
20
|
+
<div class={clsx(streamdown.theme.table.base, className)}>
|
|
21
|
+
<table class={streamdown.theme.table.table} {...props}>
|
|
22
22
|
{@render children()}
|
|
23
23
|
</table>
|
|
24
24
|
</div>
|
|
@@ -2,7 +2,7 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { Element as HastElement } from 'hast';
|
|
3
3
|
export type ElementProps<T = {}> = {
|
|
4
4
|
children: Snippet;
|
|
5
|
-
className?: string;
|
|
5
|
+
className?: string[];
|
|
6
6
|
node: HastElement;
|
|
7
7
|
props: Record<string, any>;
|
|
8
8
|
} & T & Record<string, any>;
|
package/dist/Streamdown.d.ts
CHANGED
|
@@ -7,41 +7,9 @@ import type { Theme } from './theme.js';
|
|
|
7
7
|
import type { MermaidConfig } from 'mermaid';
|
|
8
8
|
import type { KatexOptions } from 'katex';
|
|
9
9
|
import type { ElementProps } from './Elements/element.js';
|
|
10
|
+
type PredefinedElements = 'a' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'code' | 'inlineCode' | 'img' | 'ul' | 'ol' | 'li' | 'hr' | 'alert' | 'strong' | 'blockquote' | 'table' | 'thead' | 'tbody' | 'tr' | 'th' | 'td' | 'sup' | 'sub' | 'pre' | 'mermaid' | 'math' | 'inlineMath' | 'em' | 'del';
|
|
10
11
|
export type Snippets = {
|
|
11
|
-
|
|
12
|
-
h1?: Snippet<[ElementProps]>;
|
|
13
|
-
h2?: Snippet<[ElementProps]>;
|
|
14
|
-
h3?: Snippet<[ElementProps]>;
|
|
15
|
-
h4?: Snippet<[ElementProps]>;
|
|
16
|
-
h5?: Snippet<[ElementProps]>;
|
|
17
|
-
h6?: Snippet<[ElementProps]>;
|
|
18
|
-
p?: Snippet<[ElementProps]>;
|
|
19
|
-
code?: Snippet<[ElementProps]>;
|
|
20
|
-
inlineCode?: Snippet<[ElementProps]>;
|
|
21
|
-
img?: Snippet<[ElementProps]>;
|
|
22
|
-
ul?: Snippet<[ElementProps]>;
|
|
23
|
-
ol?: Snippet<[ElementProps]>;
|
|
24
|
-
li?: Snippet<[ElementProps]>;
|
|
25
|
-
hr?: Snippet<[ElementProps]>;
|
|
26
|
-
alert?: Snippet<[ElementProps]>;
|
|
27
|
-
strong?: Snippet<[ElementProps]>;
|
|
28
|
-
blockquote?: Snippet<[ElementProps]>;
|
|
29
|
-
table?: Snippet<[ElementProps]>;
|
|
30
|
-
thead?: Snippet<[ElementProps]>;
|
|
31
|
-
tbody?: Snippet<[ElementProps]>;
|
|
32
|
-
tr?: Snippet<[ElementProps]>;
|
|
33
|
-
th?: Snippet<[ElementProps]>;
|
|
34
|
-
td?: Snippet<[ElementProps]>;
|
|
35
|
-
sup?: Snippet<[ElementProps]>;
|
|
36
|
-
sub?: Snippet<[ElementProps]>;
|
|
37
|
-
pre?: Snippet<[ElementProps]>;
|
|
38
|
-
mermaid?: Snippet<[ElementProps]>;
|
|
39
|
-
math?: Snippet<[ElementProps]>;
|
|
40
|
-
inlineMath?: Snippet<[ElementProps]>;
|
|
41
|
-
em?: Snippet<[ElementProps]>;
|
|
42
|
-
ins?: Snippet<[ElementProps]>;
|
|
43
|
-
del?: Snippet<[ElementProps]>;
|
|
44
|
-
small?: Snippet<[ElementProps]>;
|
|
12
|
+
[K in PredefinedElements]?: Snippet<[ElementProps]>;
|
|
45
13
|
};
|
|
46
14
|
/**
|
|
47
15
|
* Filter elements callback type
|
|
@@ -68,7 +36,10 @@ export type StreamdownProps = {
|
|
|
68
36
|
unwrapDisallowed?: boolean;
|
|
69
37
|
urlTransform?: UrlTransform | null;
|
|
70
38
|
theme?: Partial<Theme>;
|
|
39
|
+
baseTheme?: 'tailwind' | 'shadcn';
|
|
40
|
+
mergeTheme?: boolean;
|
|
71
41
|
shikiTheme?: BundledTheme;
|
|
42
|
+
shikiPreloadThemes?: BundledTheme[];
|
|
72
43
|
mermaidConfig?: MermaidConfig;
|
|
73
44
|
katexConfig?: KatexOptions | ((inline: boolean) => KatexOptions);
|
|
74
45
|
translations?: {
|
|
@@ -80,4 +51,6 @@ export type StreamdownProps = {
|
|
|
80
51
|
important?: string;
|
|
81
52
|
};
|
|
82
53
|
};
|
|
83
|
-
|
|
54
|
+
customElements?: Record<string, Snippet<[ElementProps]>>;
|
|
55
|
+
} & Partial<Snippets>;
|
|
56
|
+
export {};
|
package/dist/Streamdown.svelte
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
extends Omit<StreamdownProps, keyof Snippets | 'class' | 'theme' | 'shikiTheme'> {
|
|
6
6
|
snippets: Snippets;
|
|
7
7
|
shikiTheme: BundledTheme;
|
|
8
|
-
// make it non optional
|
|
9
8
|
theme: Theme;
|
|
10
9
|
}
|
|
11
10
|
export class StreamdownContext {
|
|
@@ -29,13 +28,15 @@
|
|
|
29
28
|
import { bind } from './utils/bind.js';
|
|
30
29
|
import { parseMarkdownIntoBlocks } from './utils/parse-blocks.js';
|
|
31
30
|
import 'katex/dist/katex.min.css';
|
|
32
|
-
import { mergeTheme, type Theme } from './theme.js';
|
|
31
|
+
import { mergeTheme, type Theme, shadcnTheme } from './theme.js';
|
|
33
32
|
import type { BundledTheme } from 'shiki';
|
|
33
|
+
import { highlighter } from './hightlighter.svelte.js';
|
|
34
34
|
|
|
35
35
|
let {
|
|
36
36
|
content,
|
|
37
37
|
class: className,
|
|
38
38
|
shikiTheme,
|
|
39
|
+
shikiPreloadThemes,
|
|
39
40
|
parseIncompleteMarkdown,
|
|
40
41
|
defaultOrigin,
|
|
41
42
|
allowedLinkPrefixes = ['*'],
|
|
@@ -53,6 +54,9 @@
|
|
|
53
54
|
mermaidConfig,
|
|
54
55
|
katexConfig,
|
|
55
56
|
translations,
|
|
57
|
+
baseTheme,
|
|
58
|
+
mergeTheme: shouldMergeTheme = true,
|
|
59
|
+
customElements,
|
|
56
60
|
...snippets
|
|
57
61
|
}: StreamdownProps = $props();
|
|
58
62
|
|
|
@@ -106,7 +110,12 @@
|
|
|
106
110
|
return snippets;
|
|
107
111
|
},
|
|
108
112
|
get theme() {
|
|
109
|
-
return
|
|
113
|
+
return shouldMergeTheme
|
|
114
|
+
? mergeTheme(theme, baseTheme)
|
|
115
|
+
: theme || (baseTheme === 'shadcn' ? shadcnTheme : theme);
|
|
116
|
+
},
|
|
117
|
+
get baseTheme() {
|
|
118
|
+
return baseTheme;
|
|
110
119
|
},
|
|
111
120
|
get mermaidConfig() {
|
|
112
121
|
return mermaidConfig;
|
|
@@ -116,11 +125,21 @@
|
|
|
116
125
|
},
|
|
117
126
|
get translations() {
|
|
118
127
|
return translations;
|
|
128
|
+
},
|
|
129
|
+
get customElements() {
|
|
130
|
+
return customElements;
|
|
119
131
|
}
|
|
120
132
|
});
|
|
121
133
|
const id = $props.id();
|
|
122
134
|
|
|
123
135
|
const blocks = $derived(parseMarkdownIntoBlocks(content));
|
|
136
|
+
|
|
137
|
+
// Preload themes if specified to avoid flickering
|
|
138
|
+
$effect(() => {
|
|
139
|
+
if (shikiPreloadThemes && shikiPreloadThemes.length > 0) {
|
|
140
|
+
highlighter.preloadThemes(shikiPreloadThemes);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
124
143
|
</script>
|
|
125
144
|
|
|
126
145
|
{#each blocks as block, index (`${id}-block-${index}`)}
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { type BundledLanguage, type BundledTheme } from 'shiki';
|
|
2
2
|
declare class HighlighterManager {
|
|
3
3
|
initialized: boolean;
|
|
4
|
-
|
|
5
|
-
private
|
|
6
|
-
private
|
|
7
|
-
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
private getReadyKey;
|
|
4
|
+
private state;
|
|
5
|
+
private readonly preloadedThemes;
|
|
6
|
+
private readonly readyCache;
|
|
7
|
+
initPromise: Promise<void> | null;
|
|
8
|
+
private getCacheKey;
|
|
9
|
+
isLoaded(theme: BundledTheme, language: BundledLanguage): boolean;
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
13
|
-
* This
|
|
11
|
+
* Preloads themes by creating minimal highlighter instances.
|
|
12
|
+
* This reduces flickering when switching themes.
|
|
13
|
+
*/
|
|
14
|
+
preloadThemes(themes: BundledTheme[]): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Ensures the highlighter is ready for the given theme and language.
|
|
14
17
|
*/
|
|
15
18
|
isReady(theme: BundledTheme, language: BundledLanguage): Promise<void>;
|
|
16
|
-
private
|
|
19
|
+
private initialize;
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @throws {Error} If isReady() hasn't been called for this combination of themes and language
|
|
21
|
+
* Highlights code synchronously. Must call isReady() first.
|
|
20
22
|
*/
|
|
21
23
|
highlightCode(code: string, language: BundledLanguage, theme: BundledTheme, preClassName?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Clean up resources
|
|
26
|
+
*/
|
|
27
|
+
dispose(): void;
|
|
22
28
|
}
|
|
23
29
|
export { HighlighterManager };
|
|
24
30
|
export declare const highlighter: HighlighterManager;
|