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.
Files changed (40) hide show
  1. package/README.md +206 -41
  2. package/dist/Block.svelte +5 -2
  3. package/dist/Block.svelte.d.ts +2 -0
  4. package/dist/Elements/Alert.svelte +2 -1
  5. package/dist/Elements/Citation.svelte +7 -0
  6. package/dist/Elements/Code.svelte +55 -19
  7. package/dist/Elements/Code.svelte.d.ts +2 -0
  8. package/dist/Elements/Element.svelte +28 -6
  9. package/dist/Elements/Element.svelte.d.ts +1 -0
  10. package/dist/Elements/FootnoteRef.svelte +1 -0
  11. package/dist/Elements/Image.svelte +3 -2
  12. package/dist/Elements/Link.svelte +2 -2
  13. package/dist/Elements/Mermaid.svelte +62 -12
  14. package/dist/Elements/Mermaid.svelte.d.ts +2 -0
  15. package/dist/Elements/MermaidDownload.svelte +29 -8
  16. package/dist/Elements/MermaidDownload.svelte.d.ts +2 -0
  17. package/dist/Elements/TableDownload.svelte +57 -83
  18. package/dist/Elements/fallbacks/CodeFallback.svelte +21 -2
  19. package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +2 -0
  20. package/dist/Elements/fallbacks/MermaidFallback.svelte +5 -2
  21. package/dist/Elements/fallbacks/MermaidFallback.svelte.d.ts +2 -0
  22. package/dist/Elements/icons.js +10 -1
  23. package/dist/Elements/srOnly.d.ts +1 -0
  24. package/dist/Elements/srOnly.js +3 -0
  25. package/dist/Streamdown.svelte +68 -13
  26. package/dist/context.svelte.d.ts +98 -22
  27. package/dist/context.svelte.js +38 -0
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.js +2 -1
  30. package/dist/marked/index.js +25 -10
  31. package/dist/marked/marked-math.js +40 -1
  32. package/dist/utils/fence.d.ts +16 -0
  33. package/dist/utils/fence.js +39 -0
  34. package/dist/utils/parse-incomplete-markdown.d.ts +5 -1
  35. package/dist/utils/parse-incomplete-markdown.js +281 -138
  36. package/dist/utils/table-export.d.ts +14 -0
  37. package/dist/utils/table-export.js +82 -0
  38. package/dist/utils/usePinnedScroll.svelte.d.ts +22 -0
  39. package/dist/utils/usePinnedScroll.svelte.js +36 -0
  40. package/package.json +3 -2
@@ -0,0 +1,82 @@
1
+ // textContent flattens a real <br> element, so a multiline cell collapses to
2
+ // 'Paragraph one.Paragraph two.'. Walk the cell instead and keep the breaks;
3
+ // the serializers below quote or escape them.
4
+ const extractCellText = (node) => {
5
+ if (node.nodeType === 3)
6
+ return node.textContent || '';
7
+ if (node.tagName === 'BR')
8
+ return '\n';
9
+ return Array.from(node.childNodes).map(extractCellText).join('');
10
+ };
11
+ /**
12
+ * Read a rendered table (or any element containing one) into a plain matrix.
13
+ * `colspan`/`rowspan` are expanded into empty cells so every row lines up.
14
+ */
15
+ export const extractTableData = (element) => {
16
+ const trs = Array.from(element.querySelectorAll('tr'));
17
+ const rowSpanFills = [];
18
+ const matrix = trs.map((row, rowIndex) => {
19
+ const rowData = [];
20
+ let actualCol = 0; // column position in the output matrix, not in the DOM row
21
+ Array.from(row.querySelectorAll('td, th')).forEach((cell) => {
22
+ const colSpan = parseInt(cell.getAttribute('colspan') || '1');
23
+ const rowSpan = parseInt(cell.getAttribute('rowspan') || '1');
24
+ rowData.push(extractCellText(cell));
25
+ for (let i = 0; i < colSpan - 1; i++) {
26
+ rowData.push('');
27
+ }
28
+ if (rowSpan > 1) {
29
+ for (let r = 1; r < rowSpan; r++) {
30
+ rowSpanFills.push({ rowIndex: rowIndex + r, colIndex: actualCol, colSpan });
31
+ }
32
+ }
33
+ actualCol += colSpan;
34
+ });
35
+ return rowData;
36
+ });
37
+ rowSpanFills.forEach(({ rowIndex, colIndex, colSpan }) => {
38
+ if (matrix[rowIndex]) {
39
+ matrix[rowIndex].splice(colIndex, 0, ...Array(colSpan).fill(''));
40
+ }
41
+ });
42
+ // A markdown table always renders exactly one <thead> row; a DOM-only caller
43
+ // may hand us a table without one, and then everything is a data row.
44
+ const headers = trs[0]?.closest('thead') ? (matrix.shift() ?? []) : [];
45
+ return { headers, rows: matrix };
46
+ };
47
+ const allRows = ({ headers, rows }) => (headers.length ? [headers, ...rows] : rows);
48
+ // Spreadsheets in locales that write 1,1 for one-and-a-tenth read ';' as the
49
+ // column separator; Intl is the only locale source we have without a dependency.
50
+ const resolveSeparator = (separator) => separator === 'auto' ? (Intl.NumberFormat().format(1.1).includes(',') ? ';' : ',') : separator;
51
+ export const tableDataToCSV = (data, separator = ',') => {
52
+ const sep = resolveSeparator(separator);
53
+ const escape = (value) => value.includes(sep) || /["\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
54
+ return allRows(data)
55
+ .map((row) => row.map(escape).join(sep))
56
+ .join('\n');
57
+ };
58
+ export const tableDataToTSV = (data) => {
59
+ // TSV has no quoting, so a tab or a break inside a cell has to become an escape.
60
+ const escape = (value) => value.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n');
61
+ return allRows(data)
62
+ .map((row) => row.map(escape).join('\t'))
63
+ .join('\n');
64
+ };
65
+ export const tableDataToMarkdown = (data) => {
66
+ const { headers, rows } = data;
67
+ if (headers.length === 0)
68
+ return '';
69
+ // Backslashes first: escaping pipes adds backslashes of its own.
70
+ const escape = (cell) => cell.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\n/g, '<br>');
71
+ const line = (cells) => `| ${headers.map((_, i) => escape(cells[i] ?? '')).join(' | ')} |`;
72
+ return [line(headers), `| ${headers.map(() => '---').join(' | ')} |`, ...rows.map(line)].join('\n');
73
+ };
74
+ export const tableDataToHTML = (data) => {
75
+ const escape = (cell) => cell.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
76
+ const row = (cells, tag) => `<tr>${cells.map((cell) => `<${tag}>${escape(cell)}</${tag}>`).join('')}</tr>`;
77
+ const head = data.headers.length ? `<thead>${row(data.headers, 'th')}</thead>` : '';
78
+ const body = data.rows.length
79
+ ? `<tbody>${data.rows.map((r) => row(r, 'td')).join('')}</tbody>`
80
+ : '';
81
+ return `<table>${head}${body}</table>`;
82
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The whole pin/unpin decision, kept pure so it can be tested without a DOM.
3
+ * The user scrolling away from the bottom is what unpins; scrolling back pins
4
+ * again. There is no separate latch: "was the user at the bottom last time they
5
+ * scrolled" is the latch.
6
+ */
7
+ export declare const isAtBottom: (scrollTop: number, scrollHeight: number, clientHeight: number) => boolean;
8
+ export interface PinnedScrollOptions {
9
+ /** The resolved max-height; pinning is off (and costs nothing) without one. */
10
+ readonly maxHeight: string | undefined;
11
+ /** Anything that changes when the content grows — re-runs the scroll. */
12
+ readonly content: unknown;
13
+ }
14
+ /**
15
+ * Attachment that keeps a capped scroll container stuck to the bottom while its
16
+ * content grows, unless the user has scrolled up.
17
+ *
18
+ * Scrolling is always instant: this fires on every streamed chunk, so a smooth
19
+ * animation would never finish before the next one starts (and would ignore
20
+ * `prefers-reduced-motion`).
21
+ */
22
+ export declare const usePinnedScroll: (options: PinnedScrollOptions) => (node: HTMLElement) => (() => void) | undefined;
@@ -0,0 +1,36 @@
1
+ import { on } from 'svelte/events';
2
+ // A scroll position this close to the bottom counts as "at the bottom": browsers
3
+ // report fractional scrollTop on zoomed/hi-dpi displays, so an exact compare
4
+ // would unpin the container on its own.
5
+ const BOTTOM_THRESHOLD_PX = 8;
6
+ /**
7
+ * The whole pin/unpin decision, kept pure so it can be tested without a DOM.
8
+ * The user scrolling away from the bottom is what unpins; scrolling back pins
9
+ * again. There is no separate latch: "was the user at the bottom last time they
10
+ * scrolled" is the latch.
11
+ */
12
+ export const isAtBottom = (scrollTop, scrollHeight, clientHeight) => scrollHeight - scrollTop - clientHeight < BOTTOM_THRESHOLD_PX;
13
+ /**
14
+ * Attachment that keeps a capped scroll container stuck to the bottom while its
15
+ * content grows, unless the user has scrolled up.
16
+ *
17
+ * Scrolling is always instant: this fires on every streamed chunk, so a smooth
18
+ * animation would never finish before the next one starts (and would ignore
19
+ * `prefers-reduced-motion`).
20
+ */
21
+ export const usePinnedScroll = (options) => {
22
+ return (node) => {
23
+ if (!options.maxHeight)
24
+ return;
25
+ let pinned = true;
26
+ const off = on(node, 'scroll', () => {
27
+ pinned = isAtBottom(node.scrollTop, node.scrollHeight, node.clientHeight);
28
+ }, { passive: true });
29
+ $effect(() => {
30
+ options.content;
31
+ if (pinned)
32
+ node.scrollTop = node.scrollHeight;
33
+ });
34
+ return off;
35
+ };
36
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "packageManager": "pnpm@10.32.1",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,7 +23,8 @@
23
23
  "test:ui": "vitest --ui",
24
24
  "test:unit": "vitest",
25
25
  "test:browser": "vitest --run --project client",
26
- "test": "npm run test:unit -- --run --project server"
26
+ "test": "npm run test:unit -- --run --project server",
27
+ "test:size": "vitest --run --project server src/tests/bundle-size.test.ts"
27
28
  },
28
29
  "files": [
29
30
  "dist",