svelte-streamdown 2.4.5 → 2.5.1

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.
Files changed (46) hide show
  1. package/README.md +98 -4
  2. package/dist/Block.svelte +4 -4
  3. package/dist/Block.svelte.d.ts +0 -1
  4. package/dist/Elements/Alert.svelte +4 -1
  5. package/dist/Elements/Alert.svelte.d.ts +1 -0
  6. package/dist/Elements/Citation.svelte +260 -0
  7. package/dist/Elements/Citation.svelte.d.ts +7 -0
  8. package/dist/Elements/Code.svelte +13 -41
  9. package/dist/Elements/Code.svelte.d.ts +1 -0
  10. package/dist/Elements/Element.svelte +65 -33
  11. package/dist/Elements/FootnoteRef.svelte +41 -89
  12. package/dist/Elements/Image.svelte +5 -1
  13. package/dist/Elements/Image.svelte.d.ts +1 -0
  14. package/dist/Elements/Link.svelte +5 -1
  15. package/dist/Elements/Link.svelte.d.ts +1 -0
  16. package/dist/Elements/Math.svelte +10 -2
  17. package/dist/Elements/Math.svelte.d.ts +1 -0
  18. package/dist/Elements/Mermaid.svelte +87 -175
  19. package/dist/Elements/Mermaid.svelte.d.ts +1 -0
  20. package/dist/Elements/TableDownload.svelte +216 -0
  21. package/dist/Elements/TableDownload.svelte.d.ts +8 -0
  22. package/dist/Elements/icons.d.ts +9 -0
  23. package/dist/Elements/icons.js +175 -0
  24. package/dist/Elements/popover.svelte.d.ts +8 -0
  25. package/dist/Elements/popover.svelte.js +40 -0
  26. package/dist/Elements/stepperState.svelte.d.ts +35 -0
  27. package/dist/Elements/stepperState.svelte.js +98 -0
  28. package/dist/Streamdown.svelte +13 -4
  29. package/dist/Streamdown.svelte.d.ts +23 -2
  30. package/dist/context.svelte.d.ts +28 -11
  31. package/dist/context.svelte.js +14 -6
  32. package/dist/marked/index.d.ts +4 -2
  33. package/dist/marked/index.js +4 -2
  34. package/dist/marked/marked-align.d.ts +10 -0
  35. package/dist/marked/marked-align.js +36 -0
  36. package/dist/marked/marked-citations.d.ts +8 -0
  37. package/dist/marked/marked-citations.js +49 -0
  38. package/dist/marked/marked-table.js +36 -13
  39. package/dist/theme.d.ts +66 -21
  40. package/dist/theme.js +47 -17
  41. package/dist/utils/get.d.ts +1 -0
  42. package/dist/utils/get.js +25 -0
  43. package/dist/utils/panzoom.svelte.d.ts +1 -2
  44. package/dist/utils/panzoom.svelte.js +87 -43
  45. package/dist/utils/parse-incomplete-markdown.js +70 -1
  46. package/package.json +1 -1
@@ -5,13 +5,16 @@
5
5
  import type { MermaidConfig } from 'mermaid';
6
6
  import { on } from 'svelte/events';
7
7
  import { usePanzoom } from '../utils/panzoom.svelte';
8
+ import { fitViewIcon, fullscreenIcon, zoomInIcon, zoomOutIcon } from './icons.js';
8
9
 
9
10
  const streamdown = useStreamdown();
10
11
 
11
12
  const {
12
- token
13
+ token,
14
+ id
13
15
  }: {
14
16
  token: Tokens.Code;
17
+ id: string;
15
18
  } = $props();
16
19
 
17
20
  let mermaid = $state<any>(null);
@@ -154,6 +157,7 @@
154
157
  const renderMermaid = async (code: string, element: HTMLElement) => {
155
158
  try {
156
159
  // Sanitize the code first
160
+ const sanitizedCode = sanitizeMermaidCode(code);
157
161
 
158
162
  // Default configuration
159
163
  const defaultConfig: MermaidConfig = {
@@ -172,8 +176,7 @@
172
176
  };
173
177
 
174
178
  // Initialize mermaid with merged config
175
- const mergedConfig = { ...defaultConfig };
176
- mermaid.initialize(mergedConfig);
179
+ mermaid.initialize(defaultConfig);
177
180
 
178
181
  const chartHash = code.split('').reduce((acc, char) => {
179
182
  return ((acc << 5) - acc + char.charCodeAt(0)) | 0;
@@ -182,186 +185,86 @@
182
185
  const uniqueId = `mermaid-${Math.abs(chartHash)}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
183
186
 
184
187
  // Render the diagram
185
- const { svg: svgString } = await mermaid.render(uniqueId, sanitizeMermaidCode(code));
186
- const svg = new DOMParser().parseFromString(svgString, 'image/svg+xml').documentElement;
187
-
188
- const svgTarget = element.querySelector('[data-mermaid-svg]')!;
189
- svg.id = uniqueId;
190
- Array.from(svg.attributes).forEach((attribute) => {
191
- svgTarget.setAttribute(attribute.name, attribute.value);
192
- });
193
- svgTarget.innerHTML = svg.innerHTML;
194
-
195
- panzoom.zoomToFit();
196
- panzoom.zoomToFit();
188
+ const { svg: svgString } = await mermaid.render(uniqueId, sanitizedCode);
189
+
190
+ // Insert the SVG into the target element
191
+ const svgTarget = element.querySelector('[data-mermaid-svg]') as SVGElement;
192
+ if (svgTarget) {
193
+ svgTarget.innerHTML = svgString;
194
+ svgTarget.id = uniqueId;
195
+
196
+ // Apply any additional attributes from the rendered SVG
197
+ const tempSvg = new DOMParser().parseFromString(svgString, 'image/svg+xml').documentElement;
198
+ Array.from(tempSvg.attributes).forEach((attribute) => {
199
+ if (attribute.name !== 'id') {
200
+ svgTarget.setAttribute(attribute.name, attribute.value);
201
+ }
202
+ });
203
+
204
+ panzoom.zoomToFit();
205
+ panzoom.zoomToFit();
206
+ }
197
207
  } catch (err) {
198
- const sanitizedCode = sanitizeMermaidCode(code);
208
+ console.warn('Mermaid rendering error:', err);
209
+ // Could show error state in UI here
199
210
  }
200
211
  };
201
212
  </script>
202
213
 
203
- {#if mermaid}
204
- <div
205
- style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
206
- class={streamdown.theme.mermaid.base}
207
- {@attach (node) => renderMermaid(token.text, node)}
208
- {@attach insider.attach}
209
- data-expanded={'false'}
210
- >
211
- {#if streamdown.controls.mermaid}
212
- <div class={streamdown.theme.mermaid.buttons}>
213
- <button
214
- class={streamdown.theme.mermaid.button}
215
- aria-label="Zoom to fit"
216
- onclick={() => panzoom.zoomToFit()}
217
- data-panzoom-ignore
218
- >
219
- {@render (streamdown.icons?.fitView || fitViewIcon)()}
220
- </button>
221
- <button
222
- class={streamdown.theme.mermaid.button}
223
- aria-label="Zoom in"
224
- onclick={() => panzoom.zoomIn()}
225
- data-panzoom-ignore
226
- >
227
- {@render (streamdown.icons?.zoomIn || zoomInIcon)()}
228
- </button>
229
- <button
230
- class={streamdown.theme.mermaid.button}
231
- aria-label="Zoom out"
232
- onclick={() => panzoom.zoomOut()}
233
- data-panzoom-ignore
234
- >
235
- {@render (streamdown.icons?.zoomOut || zoomOutIcon)()}
236
- </button>
237
- <button
238
- class={streamdown.theme.mermaid.button}
239
- aria-label="Toggle expand"
240
- onclick={() => panzoom.toggleExpand()}
241
- data-panzoom-ignore
242
- >
243
- {@render (streamdown.icons?.fullscreen || fullscreenIcon)()}
244
- </button>
245
- </div>
246
- {/if}
247
- <svg {@attach panzoom.attach} data-mermaid-svg></svg>
248
- </div>
249
- {:else}
250
- <div class={streamdown.theme.mermaid.base}></div>
251
- {/if}
252
-
253
- {#snippet zoomInIcon()}
254
- <svg
255
- class={streamdown.theme.mermaid.icon}
256
- xmlns="http://www.w3.org/2000/svg"
257
- viewBox="0 0 24 24"
258
- fill="none"
259
- stroke="currentColor"
260
- stroke-width="2"
261
- stroke-linecap="round"
262
- stroke-linejoin="round"
263
- >
264
- <circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" />
265
- <line x1="11" x2="11" y1="8" y2="14" />
266
- <line x1="8" x2="14" y1="11" y2="11" />
267
- </svg>
268
- {/snippet}
269
-
270
- {#snippet zoomOutIcon()}
271
- <svg
272
- class={streamdown.theme.mermaid.icon}
273
- xmlns="http://www.w3.org/2000/svg"
274
- viewBox="0 0 24 24"
275
- fill="none"
276
- stroke="currentColor"
277
- stroke-width="2"
278
- stroke-linecap="round"
279
- stroke-linejoin="round"
280
- >
281
- <circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line
282
- x1="8"
283
- x2="14"
284
- y1="11"
285
- y2="11"
286
- />
287
- </svg>
288
- {/snippet}
289
-
290
- {#snippet fitViewIcon()}
291
- <svg
292
- class={streamdown.theme.mermaid.icon}
293
- xmlns="http://www.w3.org/2000/svg"
294
- viewBox="0 0 24 24"
295
- fill="none"
296
- stroke="currentColor"
297
- stroke-width="2"
298
- stroke-linecap="round"
299
- stroke-linejoin="round"
300
- >
301
- <path d="M3 7V5a2 2 0 0 1 2-2h2" />
302
- <path d="M17 3h2a2 2 0 0 1 2 2v2" /><path d="M21 17v2a2 2 0 0 1-2 2h-2" />
303
- <path d="M7 21H5a2 2 0 0 1-2-2v-2" /><rect width="10" height="8" x="7" y="8" rx="1" />
304
- </svg>
305
- {/snippet}
306
-
307
- {#snippet fullscreenIcon()}
308
- <svg
309
- class={streamdown.theme.mermaid.icon}
310
- xmlns="http://www.w3.org/2000/svg"
311
- viewBox="0 0 24 24"
312
- fill="none"
313
- stroke="currentColor"
314
- stroke-width="2"
315
- stroke-linecap="round"
316
- stroke-linejoin="round"
317
- >
318
- <path d="m15 15 6 6" /><path d="m15 9 6-6" /><path d="M21 16v5h-5" /><path d="M21 8V3h-5" />
319
- <path d="M3 16v5h5" /><path d="m3 21 6-6" /><path d="M3 8V3h5" /><path d="M9 9 3 3" />
320
- </svg>
321
- {/snippet}
214
+ <div data-streamdown-mermaid={id}>
215
+ {#if mermaid}
216
+ <div
217
+ style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
218
+ class={streamdown.theme.mermaid.base}
219
+ {@attach (node) => renderMermaid(token.text, node)}
220
+ {@attach insider.attach}
221
+ data-expanded={'false'}
222
+ >
223
+ {#if streamdown.controls.mermaid}
224
+ <div class={streamdown.theme.mermaid.buttons}>
225
+ <button
226
+ class={streamdown.theme.components.button}
227
+ aria-label="Zoom to fit"
228
+ onclick={() => panzoom.zoomToFit()}
229
+ data-panzoom-ignore
230
+ >
231
+ {@render (streamdown.icons?.fitView || fitViewIcon)()}
232
+ </button>
233
+ <button
234
+ class={streamdown.theme.components.button}
235
+ aria-label="Zoom in"
236
+ onclick={() => panzoom.zoomIn()}
237
+ data-panzoom-ignore
238
+ >
239
+ {@render (streamdown.icons?.zoomIn || zoomInIcon)()}
240
+ </button>
241
+ <button
242
+ class={streamdown.theme.components.button}
243
+ aria-label="Zoom out"
244
+ onclick={() => panzoom.zoomOut()}
245
+ data-panzoom-ignore
246
+ >
247
+ {@render (streamdown.icons?.zoomOut || zoomOutIcon)()}
248
+ </button>
249
+ <button
250
+ class={streamdown.theme.components.button}
251
+ aria-label="Toggle expand"
252
+ onclick={() => panzoom.toggleExpand()}
253
+ data-panzoom-ignore
254
+ >
255
+ {@render (streamdown.icons?.fullscreen || fullscreenIcon)()}
256
+ </button>
257
+ </div>
258
+ {/if}
259
+ <svg {@attach panzoom.attach} data-mermaid-svg></svg>
260
+ </div>
261
+ {:else}
262
+ <div class={streamdown.theme.mermaid.base}></div>
263
+ {/if}
264
+ </div>
322
265
 
323
266
  <style>
324
- /* View Transition styles for zoom effect */
325
- ::view-transition-old(panzoom-element),
326
- ::view-transition-new(panzoom-element) {
327
- /* Animate both scale and opacity for smooth zoom */
328
- animation-duration: 0.3s;
329
- animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
330
- }
331
-
332
- /* Zoom in animation (expand) */
333
- ::view-transition-old(panzoom-element) {
334
- animation-name: zoom-out;
335
- }
336
-
337
- ::view-transition-new(panzoom-element) {
338
- animation-name: zoom-in;
339
- }
340
-
341
- @keyframes zoom-out {
342
- from {
343
- transform: scale(1);
344
- }
345
- to {
346
- transform: scale(0.8);
347
- }
348
- }
349
-
350
- @keyframes zoom-in {
351
- from {
352
- transform: scale(1.2);
353
- }
354
- to {
355
- transform: scale(1);
356
- }
357
- }
358
-
359
267
  :global([data-expanded='true']) {
360
- ::view-transition-old(panzoom-element),
361
- ::view-transition-new(panzoom-element) {
362
- animation-duration: 0.3s;
363
- animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
364
- }
365
268
  position: fixed;
366
269
  top: 16px;
367
270
  left: 16px;
@@ -370,4 +273,13 @@
370
273
  z-index: 2147483647;
371
274
  margin: 0px;
372
275
  }
276
+
277
+ /* Hide Mermaid's temporary rendering containers */
278
+ :global(div[id^='dmermaid-']) {
279
+ position: absolute !important;
280
+ left: -9999px !important;
281
+ top: -9999px !important;
282
+ visibility: hidden !important;
283
+ pointer-events: none !important;
284
+ }
373
285
  </style>
@@ -1,6 +1,7 @@
1
1
  import type { Tokens } from 'marked';
2
2
  type $$ComponentProps = {
3
3
  token: Tokens.Code;
4
+ id: string;
4
5
  };
5
6
  declare const Mermaid: import("svelte").Component<$$ComponentProps, {}, "">;
6
7
  type Mermaid = ReturnType<typeof Mermaid>;
@@ -0,0 +1,216 @@
1
+ <script lang="ts">
2
+ import { useStreamdown } from '../context.svelte.js';
3
+ import { scale } from 'svelte/transition';
4
+ import { checkIcon, copyIcon, 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 type { TableToken } from '../marked/marked-table.js';
9
+ import { useCopy } from '../utils/copy.svelte.js';
10
+ import { save } from '../utils/save.js';
11
+
12
+ let {
13
+ token,
14
+ id
15
+ }: {
16
+ token: TableToken;
17
+ id: string;
18
+ } = $props();
19
+ const streamdown = useStreamdown();
20
+ const popover = new Popover();
21
+ let modeState = $state<'download' | 'copy'>('download');
22
+
23
+ useKeyDown({
24
+ keys: ['Escape'],
25
+ get isActive() {
26
+ return popover.isOpen;
27
+ },
28
+ callback: () => {
29
+ popover.isOpen = false;
30
+ }
31
+ });
32
+ const clickOutside = useClickOutside({
33
+ get isActive() {
34
+ return popover.isOpen;
35
+ },
36
+ callback: () => {
37
+ popover.isOpen = false;
38
+ }
39
+ });
40
+
41
+ let copyValue = $state(token.raw);
42
+
43
+ const copy = useCopy({
44
+ get content() {
45
+ return copyValue;
46
+ }
47
+ });
48
+
49
+ const copyOrDownload = (type: 'Markdown' | 'HTML' | 'CSV') => {
50
+ if (type === 'Markdown') {
51
+ copyValue = token.raw;
52
+ if (modeState === 'copy') {
53
+ copy.copy();
54
+ } else {
55
+ save('table.md', copyValue, 'text/markdown');
56
+ }
57
+ } else if (type === 'HTML') {
58
+ const table = document.querySelector(`[data-streamdown-table=${id}]`);
59
+
60
+ if (table) {
61
+ let html = (table.cloneNode(true) as HTMLElement).outerHTML;
62
+ // remove comments
63
+ html = html.replace(/<!--[\s\S]*?-->/g, '');
64
+ copyValue = html;
65
+ // Remove class and style attributes
66
+ html = html.replace(/class="[^"]*"/g, '');
67
+ html = html.replace(/style="[^"]*"/g, '');
68
+ // remove space between the tag name and the closing chrevron like <table > -> <table> for every tagnmae
69
+ html = html.replace(/<([^>]+)>\s+</g, '<$1><');
70
+ // Remove spaces before closing bracket >
71
+ html = html.replace(/\s+>/g, '>');
72
+ // Remove spaces after opening bracket <
73
+ html = html.replace(/<\s+/g, '<');
74
+ // Collapse multiple spaces within tags to single space
75
+ html = html.replace(/<([^>]+)>/g, (match) => match.replace(/\s+/g, ' '));
76
+
77
+ copyValue = html;
78
+
79
+ if (modeState === 'copy') {
80
+ copy.copy();
81
+ } else {
82
+ save('table.html', copyValue, 'text/html');
83
+ }
84
+ }
85
+ } else if (type === 'CSV') {
86
+ const table = document.querySelector(`[data-streamdown-table=${id}]`);
87
+
88
+ if (table) {
89
+ const rows = table.querySelectorAll('tr');
90
+ const rowSpanFills: Array<{ rowIndex: number; colIndex: number; colSpan: number }> = [];
91
+
92
+ const matrix = Array.from(rows).reduce((acc, row, rowIndex) => {
93
+ const cells = row.querySelectorAll('td, th');
94
+ const rowData: string[] = [];
95
+ let actualCol = 0; // Track actual column position in the output matrix
96
+
97
+ Array.from(cells).forEach((cell) => {
98
+ const colSpan = parseInt(cell.getAttribute('colspan') || '1');
99
+ const rowSpan = parseInt(cell.getAttribute('rowspan') || '1');
100
+
101
+ // Add the cell content
102
+ // Add the cell content, quoting if it contains commas, quotes, or newlines
103
+ const content = cell.textContent || '';
104
+ const needsQuoting = /[,"\n]/.test(content);
105
+ const escapedContent = content.replace(/"/g, '""');
106
+ rowData.push(needsQuoting ? `"${escapedContent}"` : content);
107
+
108
+ // Add empty cells for colspan
109
+ for (let i = 0; i < colSpan - 1; i++) {
110
+ rowData.push('');
111
+ }
112
+
113
+ // Track rowspan fills needed in future rows
114
+ if (rowSpan > 1) {
115
+ for (let r = 1; r < rowSpan; r++) {
116
+ rowSpanFills.push({
117
+ rowIndex: rowIndex + r,
118
+ colIndex: actualCol,
119
+ colSpan: colSpan
120
+ });
121
+ }
122
+ }
123
+
124
+ actualCol += colSpan;
125
+ });
126
+
127
+ acc.push(rowData);
128
+ return acc;
129
+ }, [] as string[][]);
130
+
131
+ // Process rowspan fills - insert empty cells at correct positions
132
+ rowSpanFills.forEach(({ rowIndex, colIndex, colSpan }) => {
133
+ if (matrix[rowIndex]) {
134
+ matrix[rowIndex].splice(colIndex, 0, ...Array(colSpan).fill(''));
135
+ }
136
+ });
137
+
138
+ const csv = matrix.map((row) => row.join(',')).join('\n');
139
+ copyValue = csv;
140
+ if (modeState === 'copy') {
141
+ copy.copy();
142
+ } else {
143
+ save('table.csv', copyValue, 'text/csv');
144
+ }
145
+ }
146
+ }
147
+ popover.isOpen = false;
148
+ };
149
+ </script>
150
+
151
+ {#if popover.isOpen}
152
+ <dialog
153
+ id={'table-download-popover'}
154
+ aria-modal="false"
155
+ transition:scale|global={{ start: 0.95, duration: 100 }}
156
+ {@attach clickOutside.attachment}
157
+ {@attach popover.popoverAttachment}
158
+ open
159
+ style:width="fit-content !important"
160
+ style:min-width="fit-content !important"
161
+ class={streamdown.theme.components.popover}
162
+ >
163
+ {#each ['Markdown', 'HTML', 'CSV'] as type}
164
+ <button
165
+ style="width: 100%; text-align: left; justify-content: flex-start; padding: 1rem 1rem; margin: 0.2rem 0;"
166
+ onclick={() => copyOrDownload(type as 'Markdown' | 'HTML' | 'CSV')}
167
+ class={streamdown.theme.components.button}
168
+ >
169
+ {type}
170
+ </button>
171
+ {/each}
172
+ </dialog>
173
+ {/if}
174
+
175
+ <div
176
+ data-streamdown-table-download
177
+ class=" right-0 ml-auto flex items-center justify-end gap-2 p-1"
178
+ >
179
+ {#each ['download', 'copy'] as mode (mode)}
180
+ <button
181
+ class={streamdown.theme.components.button}
182
+ onclick={async (e: MouseEvent) => {
183
+ if (modeState === mode && popover.isOpen) {
184
+ popover.isOpen = false;
185
+ return;
186
+ }
187
+ if (popover.isOpen && modeState !== mode) {
188
+ popover.isOpen = false;
189
+ const wait = new Promise((resolve) => {
190
+ setTimeout(resolve, 80);
191
+ });
192
+ await wait;
193
+ }
194
+ popover.reference = e.target as HTMLButtonElement;
195
+ popover.isOpen = true;
196
+ modeState = mode as 'download' | 'copy';
197
+ }}
198
+ {@attach clickOutside.attachment}
199
+ title={mode === 'download' ? 'Download table' : 'Copy table'}
200
+ >
201
+ {#if mode === 'download'}
202
+ {@render (streamdown.icons?.download || downloadIcon)()}
203
+ {:else if copy.isCopied}
204
+ {@render (streamdown.icons?.check || checkIcon)()}
205
+ {:else}
206
+ {@render (streamdown.icons?.copy || copyIcon)()}
207
+ {/if}
208
+ </button>
209
+ {/each}
210
+ </div>
211
+
212
+ <style>
213
+ :global([data-streamdown-table-download] + div) {
214
+ margin-top: 0px;
215
+ }
216
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { TableToken } from '../marked/marked-table.js';
2
+ type $$ComponentProps = {
3
+ token: TableToken;
4
+ id: string;
5
+ };
6
+ declare const TableDownload: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type TableDownload = ReturnType<typeof TableDownload>;
8
+ export default TableDownload;
@@ -0,0 +1,9 @@
1
+ export declare const copyIcon: import("svelte").Snippet<[]>;
2
+ export declare const downloadIcon: import("svelte").Snippet<[]>;
3
+ export declare const checkIcon: import("svelte").Snippet<[]>;
4
+ export declare const zoomInIcon: import("svelte").Snippet<[]>;
5
+ export declare const zoomOutIcon: import("svelte").Snippet<[]>;
6
+ export declare const fitViewIcon: import("svelte").Snippet<[]>;
7
+ export declare const fullscreenIcon: import("svelte").Snippet<[]>;
8
+ export declare const chevronRight: import("svelte").Snippet<[]>;
9
+ export declare const chevronLeft: import("svelte").Snippet<[]>;