svelte-streamdown 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -41
- package/dist/Block.svelte +5 -2
- package/dist/Block.svelte.d.ts +2 -0
- package/dist/Elements/Alert.svelte +2 -1
- package/dist/Elements/Citation.svelte +7 -0
- package/dist/Elements/Code.svelte +55 -19
- package/dist/Elements/Code.svelte.d.ts +2 -0
- package/dist/Elements/Element.svelte +28 -6
- package/dist/Elements/Element.svelte.d.ts +1 -0
- package/dist/Elements/FootnoteRef.svelte +1 -0
- package/dist/Elements/Image.svelte +3 -2
- package/dist/Elements/Link.svelte +2 -2
- package/dist/Elements/Mermaid.svelte +62 -12
- package/dist/Elements/Mermaid.svelte.d.ts +2 -0
- package/dist/Elements/MermaidDownload.svelte +29 -8
- package/dist/Elements/MermaidDownload.svelte.d.ts +2 -0
- package/dist/Elements/TableDownload.svelte +57 -83
- package/dist/Elements/fallbacks/CodeFallback.svelte +21 -2
- package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +2 -0
- package/dist/Elements/fallbacks/MermaidFallback.svelte +5 -2
- package/dist/Elements/fallbacks/MermaidFallback.svelte.d.ts +2 -0
- package/dist/Elements/icons.js +10 -1
- package/dist/Elements/srOnly.d.ts +1 -0
- package/dist/Elements/srOnly.js +3 -0
- package/dist/Streamdown.svelte +68 -13
- package/dist/context.svelte.d.ts +98 -22
- package/dist/context.svelte.js +38 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/marked/index.js +25 -10
- package/dist/marked/marked-math.js +40 -1
- package/dist/utils/fence.d.ts +16 -0
- package/dist/utils/fence.js +39 -0
- package/dist/utils/parse-incomplete-markdown.d.ts +5 -1
- package/dist/utils/parse-incomplete-markdown.js +281 -138
- package/dist/utils/table-export.d.ts +14 -0
- package/dist/utils/table-export.js +82 -0
- package/dist/utils/usePinnedScroll.svelte.d.ts +22 -0
- package/dist/utils/usePinnedScroll.svelte.js +36 -0
- package/package.json +3 -2
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
<span
|
|
52
52
|
data-streamdown-image-blocked={id}
|
|
53
53
|
class="inline-block rounded bg-gray-200 px-3 py-1 text-sm text-gray-600 dark:bg-gray-700 dark:text-gray-400"
|
|
54
|
-
title={
|
|
54
|
+
title={`${streamdown.translations.controls.blockedUrl}: ${token.href}`}
|
|
55
55
|
>
|
|
56
|
-
[
|
|
56
|
+
[{streamdown.translations.controls.imageBlocked}: {token.text ||
|
|
57
|
+
streamdown.translations.controls.imageNoDescription}]
|
|
57
58
|
</span>
|
|
58
59
|
{/if}
|
|
59
60
|
{/if}
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
<span
|
|
52
52
|
data-streamdown-link-blocked={id}
|
|
53
53
|
class={streamdown.theme.link.blocked}
|
|
54
|
-
title={
|
|
54
|
+
title={`${streamdown.translations.controls.blockedUrl}: ${token.href}`}
|
|
55
55
|
>
|
|
56
|
-
{@render children()} [
|
|
56
|
+
{@render children()} [{streamdown.translations.controls.linkBlocked}]
|
|
57
57
|
</span>
|
|
58
58
|
{/if}
|
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
14
|
token,
|
|
15
|
-
id
|
|
15
|
+
id,
|
|
16
|
+
incomplete = false
|
|
16
17
|
}: {
|
|
17
18
|
token: CodeToken;
|
|
18
19
|
id: string;
|
|
20
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
21
|
+
incomplete?: boolean;
|
|
19
22
|
} = $props();
|
|
20
23
|
|
|
21
24
|
// Trailing blank lines are noise for mermaid but they still changed `token.text`
|
|
@@ -23,6 +26,15 @@
|
|
|
23
26
|
// Code.svelte.
|
|
24
27
|
const chart = $derived(token.text.replace(/\n+$/, ''));
|
|
25
28
|
|
|
29
|
+
// Screen readers get the diagram's declaration line ('flowchart TD', 'sequenceDiagram'),
|
|
30
|
+
// which is the only human-readable summary the source offers.
|
|
31
|
+
const firstLine = $derived(chart.split('\n', 1)[0].trim());
|
|
32
|
+
const diagramLabel = $derived(
|
|
33
|
+
firstLine
|
|
34
|
+
? `${streamdown.translations.controls.diagram}: ${firstLine}`
|
|
35
|
+
: streamdown.translations.controls.diagram
|
|
36
|
+
);
|
|
37
|
+
|
|
26
38
|
let mermaid = $state<any>(null);
|
|
27
39
|
onMount(async () => {
|
|
28
40
|
mermaid = (await import('mermaid')).default;
|
|
@@ -30,7 +42,7 @@
|
|
|
30
42
|
|
|
31
43
|
const useIsInsideForMoreThanAQuarterSecond = () => {
|
|
32
44
|
let isInside = $state(false);
|
|
33
|
-
let timeout:
|
|
45
|
+
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
34
46
|
|
|
35
47
|
return {
|
|
36
48
|
get isInside() {
|
|
@@ -67,6 +79,19 @@
|
|
|
67
79
|
}
|
|
68
80
|
});
|
|
69
81
|
|
|
82
|
+
const expandLabel = $derived(
|
|
83
|
+
panzoom.expanded
|
|
84
|
+
? streamdown.translations.controls.exitFullscreen
|
|
85
|
+
: streamdown.translations.controls.fullscreen
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
let container = $state<HTMLDivElement>();
|
|
89
|
+
$effect(() => {
|
|
90
|
+
// Expanding covers the page: move focus in so Escape (handled by panzoom)
|
|
91
|
+
// and the toolbar are reachable from the keyboard.
|
|
92
|
+
if (panzoom.expanded) container?.focus();
|
|
93
|
+
});
|
|
94
|
+
|
|
70
95
|
const sanitizeMermaidCode = (code: string): string => {
|
|
71
96
|
try {
|
|
72
97
|
let sanitized = code;
|
|
@@ -199,10 +224,15 @@
|
|
|
199
224
|
svgTarget.innerHTML = svgString;
|
|
200
225
|
svgTarget.id = uniqueId;
|
|
201
226
|
|
|
202
|
-
// Apply any additional attributes from the rendered SVG
|
|
227
|
+
// Apply any additional attributes from the rendered SVG. The accessibility
|
|
228
|
+
// attributes are ours: mermaid's root carries role="graphics-document" and
|
|
229
|
+
// an aria-roledescription, which would overwrite the label a screen reader
|
|
230
|
+
// needs (and did, non-deterministically, depending on when the lazy import
|
|
231
|
+
// resolved).
|
|
232
|
+
const keepOwn = new Set(['id', 'role', 'aria-label', 'aria-roledescription']);
|
|
203
233
|
const tempSvg = new DOMParser().parseFromString(svgString, 'image/svg+xml').documentElement;
|
|
204
234
|
Array.from(tempSvg.attributes).forEach((attribute) => {
|
|
205
|
-
if (attribute.name
|
|
235
|
+
if (!keepOwn.has(attribute.name)) {
|
|
206
236
|
svgTarget.setAttribute(attribute.name, attribute.value);
|
|
207
237
|
}
|
|
208
238
|
});
|
|
@@ -217,20 +247,31 @@
|
|
|
217
247
|
};
|
|
218
248
|
</script>
|
|
219
249
|
|
|
220
|
-
<div data-streamdown-mermaid={id}>
|
|
250
|
+
<div data-streamdown-mermaid={id} data-incomplete={incomplete || undefined}>
|
|
221
251
|
{#if mermaid}
|
|
222
252
|
<div
|
|
253
|
+
bind:this={container}
|
|
223
254
|
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
224
255
|
class={streamdown.theme.mermaid.base}
|
|
225
|
-
{@attach (node) =>
|
|
256
|
+
{@attach (node) => {
|
|
257
|
+
// A half-written diagram makes mermaid throw and log on every chunk, so
|
|
258
|
+
// skip the render entirely and keep showing whatever rendered last.
|
|
259
|
+
if (!incomplete) renderMermaid(chart, node);
|
|
260
|
+
}}
|
|
226
261
|
{@attach insider.attach}
|
|
227
262
|
data-expanded={'false'}
|
|
263
|
+
role={panzoom.expanded ? 'dialog' : undefined}
|
|
264
|
+
aria-modal={panzoom.expanded ? 'true' : undefined}
|
|
265
|
+
aria-label={panzoom.expanded ? diagramLabel : undefined}
|
|
266
|
+
tabindex="-1"
|
|
228
267
|
>
|
|
229
268
|
{#if streamdown.controls.mermaid}
|
|
230
269
|
<div class={streamdown.theme.mermaid.buttons}>
|
|
231
270
|
<button
|
|
232
271
|
class={streamdown.theme.components.button}
|
|
233
|
-
aria-label=
|
|
272
|
+
aria-label={streamdown.translations.controls.resetView}
|
|
273
|
+
title={streamdown.translations.controls.resetView}
|
|
274
|
+
type="button"
|
|
234
275
|
onclick={() => panzoom.zoomToFit()}
|
|
235
276
|
data-panzoom-ignore
|
|
236
277
|
>
|
|
@@ -238,7 +279,9 @@
|
|
|
238
279
|
</button>
|
|
239
280
|
<button
|
|
240
281
|
class={streamdown.theme.components.button}
|
|
241
|
-
aria-label=
|
|
282
|
+
aria-label={streamdown.translations.controls.zoomIn}
|
|
283
|
+
title={streamdown.translations.controls.zoomIn}
|
|
284
|
+
type="button"
|
|
242
285
|
onclick={() => panzoom.zoomIn()}
|
|
243
286
|
data-panzoom-ignore
|
|
244
287
|
>
|
|
@@ -246,7 +289,9 @@
|
|
|
246
289
|
</button>
|
|
247
290
|
<button
|
|
248
291
|
class={streamdown.theme.components.button}
|
|
249
|
-
aria-label=
|
|
292
|
+
aria-label={streamdown.translations.controls.zoomOut}
|
|
293
|
+
title={streamdown.translations.controls.zoomOut}
|
|
294
|
+
type="button"
|
|
250
295
|
onclick={() => panzoom.zoomOut()}
|
|
251
296
|
data-panzoom-ignore
|
|
252
297
|
>
|
|
@@ -254,16 +299,21 @@
|
|
|
254
299
|
</button>
|
|
255
300
|
<button
|
|
256
301
|
class={streamdown.theme.components.button}
|
|
257
|
-
aria-label=
|
|
302
|
+
aria-label={expandLabel}
|
|
303
|
+
title={expandLabel}
|
|
304
|
+
aria-pressed={panzoom.expanded}
|
|
305
|
+
type="button"
|
|
258
306
|
onclick={() => panzoom.toggleExpand()}
|
|
259
307
|
data-panzoom-ignore
|
|
260
308
|
>
|
|
261
309
|
{@render (streamdown.icons?.fullscreen || fullscreenIcon)()}
|
|
262
310
|
</button>
|
|
263
|
-
|
|
311
|
+
{#if streamdown.controls.mermaidDownload}
|
|
312
|
+
<MermaidDownload {token} {id} />
|
|
313
|
+
{/if}
|
|
264
314
|
</div>
|
|
265
315
|
{/if}
|
|
266
|
-
<svg {@attach panzoom.attach} data-mermaid-svg></svg>
|
|
316
|
+
<svg {@attach panzoom.attach} data-mermaid-svg role="img" aria-label={diagramLabel}></svg>
|
|
267
317
|
</div>
|
|
268
318
|
{:else}
|
|
269
319
|
<div class={streamdown.theme.mermaid.base}></div>
|
|
@@ -2,6 +2,8 @@ import type { CodeToken } from '../marked/index.js';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
token: CodeToken;
|
|
4
4
|
id: string;
|
|
5
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
|
+
incomplete?: boolean;
|
|
5
7
|
};
|
|
6
8
|
declare const Mermaid: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
9
|
type Mermaid = ReturnType<typeof Mermaid>;
|
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
import { useClickOutside } from '../utils/useClickOutside.svelte.js';
|
|
7
7
|
import { useKeyDown } from '../utils/useKeyDown.svelte.js';
|
|
8
8
|
import { save } from '../utils/save.js';
|
|
9
|
+
import type { CodeToken } from '../marked/index.js';
|
|
9
10
|
|
|
10
11
|
let {
|
|
12
|
+
token,
|
|
11
13
|
id
|
|
12
14
|
}: {
|
|
15
|
+
token: CodeToken;
|
|
13
16
|
id: string;
|
|
14
17
|
} = $props();
|
|
15
18
|
|
|
16
19
|
const streamdown = useStreamdown();
|
|
17
20
|
const popover = new Popover();
|
|
18
21
|
|
|
22
|
+
const filename = (extension: string) => {
|
|
23
|
+
const base = streamdown.controls.mermaidDownloadFilename;
|
|
24
|
+
return `${typeof base === 'function' ? base(token) : base}.${extension}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
useKeyDown({
|
|
20
28
|
keys: ['Escape'],
|
|
21
29
|
get isActive() {
|
|
@@ -68,7 +76,7 @@
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
const svgString = new XMLSerializer().serializeToString(clonedSvg);
|
|
71
|
-
save('
|
|
79
|
+
save(filename('svg'), svgString, 'image/svg+xml');
|
|
72
80
|
popover.isOpen = false;
|
|
73
81
|
};
|
|
74
82
|
|
|
@@ -128,7 +136,7 @@
|
|
|
128
136
|
const url = URL.createObjectURL(blob);
|
|
129
137
|
const link = document.createElement('a');
|
|
130
138
|
link.href = url;
|
|
131
|
-
link.download = '
|
|
139
|
+
link.download = filename('png');
|
|
132
140
|
document.body.appendChild(link);
|
|
133
141
|
link.click();
|
|
134
142
|
document.body.removeChild(link);
|
|
@@ -145,11 +153,21 @@
|
|
|
145
153
|
popover.isOpen = false;
|
|
146
154
|
};
|
|
147
155
|
|
|
148
|
-
const
|
|
156
|
+
const formats = $derived([
|
|
157
|
+
{ type: 'PNG', label: streamdown.translations.controls.downloadDiagramPng },
|
|
158
|
+
{ type: 'SVG', label: streamdown.translations.controls.downloadDiagramSvg },
|
|
159
|
+
{ type: 'MMD', label: streamdown.translations.controls.downloadDiagramMmd }
|
|
160
|
+
] as const);
|
|
161
|
+
|
|
162
|
+
const download = (type: 'SVG' | 'PNG' | 'MMD') => {
|
|
149
163
|
if (type === 'SVG') {
|
|
150
164
|
downloadSvg();
|
|
151
|
-
} else {
|
|
165
|
+
} else if (type === 'PNG') {
|
|
152
166
|
downloadPng();
|
|
167
|
+
} else {
|
|
168
|
+
// The diagram source, same trailing-newline trim as the rendered chart.
|
|
169
|
+
save(filename('mmd'), token.text.replace(/\n+$/, ''), 'text/plain');
|
|
170
|
+
popover.isOpen = false;
|
|
153
171
|
}
|
|
154
172
|
};
|
|
155
173
|
</script>
|
|
@@ -166,13 +184,14 @@
|
|
|
166
184
|
style:min-width="fit-content !important"
|
|
167
185
|
class={streamdown.theme.components.popover}
|
|
168
186
|
>
|
|
169
|
-
{#each
|
|
187
|
+
{#each formats as { type, label } (type)}
|
|
170
188
|
<button
|
|
171
189
|
style="width: 100%; text-align: left; justify-content: flex-start; padding: 1rem 1rem; margin: 0.2rem 0;"
|
|
172
|
-
onclick={() => download(type
|
|
190
|
+
onclick={() => download(type)}
|
|
173
191
|
class={streamdown.theme.components.button}
|
|
192
|
+
type="button"
|
|
174
193
|
>
|
|
175
|
-
{
|
|
194
|
+
{label}
|
|
176
195
|
</button>
|
|
177
196
|
{/each}
|
|
178
197
|
</dialog>
|
|
@@ -189,7 +208,9 @@
|
|
|
189
208
|
popover.isOpen = true;
|
|
190
209
|
}}
|
|
191
210
|
{@attach clickOutside.attachment}
|
|
192
|
-
|
|
211
|
+
type="button"
|
|
212
|
+
title={streamdown.translations.controls.downloadDiagram}
|
|
213
|
+
aria-label={streamdown.translations.controls.downloadDiagram}
|
|
193
214
|
data-panzoom-ignore
|
|
194
215
|
>
|
|
195
216
|
{@render (streamdown.icons?.download || downloadIcon)()}
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import type { TableToken } from '../marked/marked-table.js';
|
|
9
9
|
import { useCopy } from '../utils/copy.svelte.js';
|
|
10
10
|
import { save } from '../utils/save.js';
|
|
11
|
+
import { srOnly } from './srOnly.js';
|
|
12
|
+
import { extractTableData, tableDataToCSV, tableDataToTSV } from '../utils/table-export.js';
|
|
13
|
+
|
|
14
|
+
type Format = 'Markdown' | 'HTML' | 'CSV' | 'TSV';
|
|
11
15
|
|
|
12
16
|
let {
|
|
13
17
|
token,
|
|
@@ -19,6 +23,11 @@
|
|
|
19
23
|
const streamdown = useStreamdown();
|
|
20
24
|
const popover = new Popover();
|
|
21
25
|
let modeState = $state<'download' | 'copy'>('download');
|
|
26
|
+
const modes = $derived(
|
|
27
|
+
(['download', 'copy'] as const).filter((mode) =>
|
|
28
|
+
mode === 'download' ? streamdown.controls.tableDownload : streamdown.controls.tableCopy
|
|
29
|
+
)
|
|
30
|
+
);
|
|
22
31
|
|
|
23
32
|
useKeyDown({
|
|
24
33
|
keys: ['Escape'],
|
|
@@ -46,31 +55,41 @@
|
|
|
46
55
|
}
|
|
47
56
|
});
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const filename = (extension: string) => {
|
|
59
|
+
const base = streamdown.controls.tableDownloadFilename;
|
|
60
|
+
return `${typeof base === 'function' ? base(token) : base}.${extension}`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formats = $derived([
|
|
64
|
+
{ type: 'Markdown', label: streamdown.translations.controls.tableFormatMarkdown },
|
|
65
|
+
{ type: 'HTML', label: streamdown.translations.controls.tableFormatHtml },
|
|
66
|
+
{ type: 'CSV', label: streamdown.translations.controls.tableFormatCsv },
|
|
67
|
+
{ type: 'TSV', label: streamdown.translations.controls.tableFormatTsv }
|
|
68
|
+
] as const satisfies readonly { type: Format; label: string }[]);
|
|
69
|
+
|
|
70
|
+
const emit = (content: string, extension: string, mimeType: string) => {
|
|
71
|
+
copyValue = content;
|
|
72
|
+
if (modeState === 'copy') {
|
|
73
|
+
copy.copy();
|
|
74
|
+
} else {
|
|
75
|
+
save(filename(extension), content, mimeType);
|
|
76
|
+
}
|
|
56
77
|
};
|
|
57
78
|
|
|
58
|
-
const
|
|
79
|
+
const tableElement = () => document.querySelector(`[data-streamdown-table="${id}"]`);
|
|
80
|
+
|
|
81
|
+
const copyOrDownload = (type: Format) => {
|
|
59
82
|
if (type === 'Markdown') {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} else {
|
|
64
|
-
save('table.md', copyValue, 'text/markdown');
|
|
65
|
-
}
|
|
83
|
+
// `token.raw` is the author's own markdown; a DOM round-trip through
|
|
84
|
+
// tableDataToMarkdown() would lose the inline formatting.
|
|
85
|
+
emit(token.raw, 'md', 'text/markdown');
|
|
66
86
|
} else if (type === 'HTML') {
|
|
67
|
-
const table =
|
|
87
|
+
const table = tableElement();
|
|
68
88
|
|
|
69
89
|
if (table) {
|
|
70
90
|
let html = (table.cloneNode(true) as HTMLElement).outerHTML;
|
|
71
91
|
// remove comments
|
|
72
92
|
html = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
73
|
-
copyValue = html;
|
|
74
93
|
// Remove class and style attributes
|
|
75
94
|
html = html.replace(/class="[^"]*"/g, '');
|
|
76
95
|
html = html.replace(/style="[^"]*"/g, '');
|
|
@@ -83,72 +102,17 @@
|
|
|
83
102
|
// Collapse multiple spaces within tags to single space
|
|
84
103
|
html = html.replace(/<([^>]+)>/g, (match) => match.replace(/\s+/g, ' '));
|
|
85
104
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (modeState === 'copy') {
|
|
89
|
-
copy.copy();
|
|
90
|
-
} else {
|
|
91
|
-
save('table.html', copyValue, 'text/html');
|
|
92
|
-
}
|
|
105
|
+
emit(html, 'html', 'text/html');
|
|
93
106
|
}
|
|
94
|
-
} else
|
|
95
|
-
const table =
|
|
107
|
+
} else {
|
|
108
|
+
const table = tableElement();
|
|
96
109
|
|
|
97
110
|
if (table) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const matrix = Array.from(rows).reduce((acc, row, rowIndex) => {
|
|
102
|
-
const cells = row.querySelectorAll('td, th');
|
|
103
|
-
const rowData: string[] = [];
|
|
104
|
-
let actualCol = 0; // Track actual column position in the output matrix
|
|
105
|
-
|
|
106
|
-
Array.from(cells).forEach((cell) => {
|
|
107
|
-
const colSpan = parseInt(cell.getAttribute('colspan') || '1');
|
|
108
|
-
const rowSpan = parseInt(cell.getAttribute('rowspan') || '1');
|
|
109
|
-
|
|
110
|
-
// Add the cell content, quoting if it contains commas, quotes, or newlines
|
|
111
|
-
const content = extractCellText(cell);
|
|
112
|
-
const needsQuoting = /[,"\n]/.test(content);
|
|
113
|
-
const escapedContent = content.replace(/"/g, '""');
|
|
114
|
-
rowData.push(needsQuoting ? `"${escapedContent}"` : content);
|
|
115
|
-
|
|
116
|
-
// Add empty cells for colspan
|
|
117
|
-
for (let i = 0; i < colSpan - 1; i++) {
|
|
118
|
-
rowData.push('');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Track rowspan fills needed in future rows
|
|
122
|
-
if (rowSpan > 1) {
|
|
123
|
-
for (let r = 1; r < rowSpan; r++) {
|
|
124
|
-
rowSpanFills.push({
|
|
125
|
-
rowIndex: rowIndex + r,
|
|
126
|
-
colIndex: actualCol,
|
|
127
|
-
colSpan: colSpan
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
actualCol += colSpan;
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
acc.push(rowData);
|
|
136
|
-
return acc;
|
|
137
|
-
}, [] as string[][]);
|
|
138
|
-
|
|
139
|
-
// Process rowspan fills - insert empty cells at correct positions
|
|
140
|
-
rowSpanFills.forEach(({ rowIndex, colIndex, colSpan }) => {
|
|
141
|
-
if (matrix[rowIndex]) {
|
|
142
|
-
matrix[rowIndex].splice(colIndex, 0, ...Array(colSpan).fill(''));
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const csv = matrix.map((row) => row.join(',')).join('\n');
|
|
147
|
-
copyValue = csv;
|
|
148
|
-
if (modeState === 'copy') {
|
|
149
|
-
copy.copy();
|
|
111
|
+
const data = extractTableData(table);
|
|
112
|
+
if (type === 'CSV') {
|
|
113
|
+
emit(tableDataToCSV(data, streamdown.controls.tableCsvSeparator), 'csv', 'text/csv');
|
|
150
114
|
} else {
|
|
151
|
-
|
|
115
|
+
emit(tableDataToTSV(data), 'tsv', 'text/tab-separated-values');
|
|
152
116
|
}
|
|
153
117
|
}
|
|
154
118
|
}
|
|
@@ -168,13 +132,14 @@
|
|
|
168
132
|
style:min-width="fit-content !important"
|
|
169
133
|
class={streamdown.theme.components.popover}
|
|
170
134
|
>
|
|
171
|
-
{#each
|
|
135
|
+
{#each formats as { type, label } (type)}
|
|
172
136
|
<button
|
|
173
137
|
style="width: 100%; text-align: left; justify-content: flex-start; padding: 1rem 1rem; margin: 0.2rem 0;"
|
|
174
|
-
onclick={() => copyOrDownload(type
|
|
138
|
+
onclick={() => copyOrDownload(type)}
|
|
175
139
|
class={streamdown.theme.components.button}
|
|
140
|
+
type="button"
|
|
176
141
|
>
|
|
177
|
-
{
|
|
142
|
+
{label}
|
|
178
143
|
</button>
|
|
179
144
|
{/each}
|
|
180
145
|
</dialog>
|
|
@@ -184,7 +149,7 @@
|
|
|
184
149
|
data-streamdown-table-download
|
|
185
150
|
class=" right-0 ml-auto flex items-center justify-end gap-2 p-1"
|
|
186
151
|
>
|
|
187
|
-
{#each
|
|
152
|
+
{#each modes as mode (mode)}
|
|
188
153
|
<button
|
|
189
154
|
class={streamdown.theme.components.button}
|
|
190
155
|
onclick={async (e: MouseEvent) => {
|
|
@@ -204,7 +169,13 @@
|
|
|
204
169
|
modeState = mode as 'download' | 'copy';
|
|
205
170
|
}}
|
|
206
171
|
{@attach clickOutside.attachment}
|
|
207
|
-
|
|
172
|
+
type="button"
|
|
173
|
+
title={mode === 'download'
|
|
174
|
+
? streamdown.translations.controls.downloadTable
|
|
175
|
+
: streamdown.translations.controls.copyTable}
|
|
176
|
+
aria-label={mode === 'download'
|
|
177
|
+
? streamdown.translations.controls.downloadTable
|
|
178
|
+
: streamdown.translations.controls.copyTable}
|
|
208
179
|
>
|
|
209
180
|
{#if mode === 'download'}
|
|
210
181
|
{@render (streamdown.icons?.download || downloadIcon)()}
|
|
@@ -215,6 +186,9 @@
|
|
|
215
186
|
{/if}
|
|
216
187
|
</button>
|
|
217
188
|
{/each}
|
|
189
|
+
<span aria-live="polite" style={srOnly}
|
|
190
|
+
>{copy.isCopied ? streamdown.translations.controls.copiedTable : ''}</span
|
|
191
|
+
>
|
|
218
192
|
</div>
|
|
219
193
|
|
|
220
194
|
<style>
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useStreamdown } from '../../context.svelte.js';
|
|
3
|
+
import { usePinnedScroll } from '../../utils/usePinnedScroll.svelte.js';
|
|
3
4
|
import type { Tokens } from 'marked';
|
|
4
5
|
|
|
5
6
|
const {
|
|
6
7
|
token,
|
|
7
|
-
id
|
|
8
|
+
id,
|
|
9
|
+
incomplete = false
|
|
8
10
|
}: {
|
|
9
11
|
token: Tokens.Code;
|
|
10
12
|
id: string;
|
|
13
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
14
|
+
incomplete?: boolean;
|
|
11
15
|
} = $props();
|
|
12
16
|
|
|
13
17
|
const streamdown = useStreamdown();
|
|
@@ -17,10 +21,21 @@
|
|
|
17
21
|
// on nearly every streamed chunk. This is the default renderer, so it needs it
|
|
18
22
|
// too.
|
|
19
23
|
const code = $derived(token.text.replace(/\n+$/, ''));
|
|
24
|
+
|
|
25
|
+
// Same scroll container as Code.svelte: `pre` already scrolls horizontally.
|
|
26
|
+
const pinnedScroll = usePinnedScroll({
|
|
27
|
+
get maxHeight() {
|
|
28
|
+
return streamdown.codeBlockMaxHeight;
|
|
29
|
+
},
|
|
30
|
+
get content() {
|
|
31
|
+
return code;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
20
34
|
</script>
|
|
21
35
|
|
|
22
36
|
<div
|
|
23
37
|
data-streamdown-code={id}
|
|
38
|
+
data-incomplete={incomplete || undefined}
|
|
24
39
|
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
25
40
|
class={streamdown.theme.code.base}
|
|
26
41
|
>
|
|
@@ -28,7 +43,11 @@
|
|
|
28
43
|
<span class={streamdown.theme.code.language}>{token.lang}</span>
|
|
29
44
|
</div>
|
|
30
45
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
31
|
-
<pre
|
|
46
|
+
<pre
|
|
47
|
+
class={streamdown.theme.code.pre}
|
|
48
|
+
style:max-height={streamdown.codeBlockMaxHeight}
|
|
49
|
+
style:overflow-y={streamdown.codeBlockMaxHeight ? 'auto' : undefined}
|
|
50
|
+
{@attach pinnedScroll}><code
|
|
32
51
|
>{#each code.split('\n') as line}<span class={streamdown.theme.code.line}
|
|
33
52
|
><span style={streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
34
53
|
>{line.trim().length > 0 ? line : '\u200B'}</span
|
|
@@ -2,6 +2,8 @@ import type { Tokens } from 'marked';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
token: Tokens.Code;
|
|
4
4
|
id: string;
|
|
5
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
|
+
incomplete?: boolean;
|
|
5
7
|
};
|
|
6
8
|
declare const CodeFallback: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
9
|
type CodeFallback = ReturnType<typeof CodeFallback>;
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
token,
|
|
7
|
-
id
|
|
7
|
+
id,
|
|
8
|
+
incomplete = false
|
|
8
9
|
}: {
|
|
9
10
|
token: Tokens.Code;
|
|
10
11
|
id: string;
|
|
12
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
13
|
+
incomplete?: boolean;
|
|
11
14
|
} = $props();
|
|
12
15
|
|
|
13
16
|
const streamdown = useStreamdown();
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
const chart = $derived(token.text.replace(/\n+$/, ''));
|
|
20
23
|
</script>
|
|
21
24
|
|
|
22
|
-
<div data-streamdown-mermaid={id}>
|
|
25
|
+
<div data-streamdown-mermaid={id} data-incomplete={incomplete || undefined}>
|
|
23
26
|
<div
|
|
24
27
|
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
25
28
|
class={streamdown.theme.code.base}
|
|
@@ -2,6 +2,8 @@ import type { Tokens } from 'marked';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
token: Tokens.Code;
|
|
4
4
|
id: string;
|
|
5
|
+
/** The fence is still being streamed; nothing below it is final yet. */
|
|
6
|
+
incomplete?: boolean;
|
|
5
7
|
};
|
|
6
8
|
declare const MermaidFallback: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
9
|
type MermaidFallback = ReturnType<typeof MermaidFallback>;
|