svelte-streamdown 4.1.0 → 4.2.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 (37) hide show
  1. package/README.md +149 -47
  2. package/dist/Block.svelte +19 -8
  3. package/dist/Block.svelte.d.ts +2 -0
  4. package/dist/Elements/Code.svelte +15 -4
  5. package/dist/Elements/Code.svelte.d.ts +2 -0
  6. package/dist/Elements/Element.svelte +32 -11
  7. package/dist/Elements/Element.svelte.d.ts +1 -0
  8. package/dist/Elements/Mermaid.svelte +5 -11
  9. package/dist/Elements/Mermaid.svelte.d.ts +1 -0
  10. package/dist/Elements/TableDownload.svelte +44 -3
  11. package/dist/Elements/TableDownload.svelte.d.ts +3 -1
  12. package/dist/Elements/fallbacks/CodeFallback.svelte +16 -6
  13. package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +3 -2
  14. package/dist/Elements/icons.d.ts +1 -0
  15. package/dist/Elements/icons.js +8 -0
  16. package/dist/Streamdown.svelte +68 -6
  17. package/dist/context.svelte.d.ts +37 -1
  18. package/dist/context.svelte.js +3 -0
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.js +2 -1
  21. package/dist/marked/index.d.ts +11 -4
  22. package/dist/marked/index.js +54 -45
  23. package/dist/marked/marked-mdx.d.ts +27 -1
  24. package/dist/marked/marked-mdx.js +37 -8
  25. package/dist/theme.d.ts +6 -0
  26. package/dist/theme.js +12 -4
  27. package/dist/utils/expand.svelte.d.ts +21 -0
  28. package/dist/utils/expand.svelte.js +46 -0
  29. package/dist/utils/fence.d.ts +12 -1
  30. package/dist/utils/fence.js +34 -17
  31. package/dist/utils/line-numbers.d.ts +23 -0
  32. package/dist/utils/line-numbers.js +30 -0
  33. package/dist/utils/normalize-html-indentation.d.ts +10 -0
  34. package/dist/utils/normalize-html-indentation.js +52 -0
  35. package/dist/utils/parse-incomplete-markdown.d.ts +20 -6
  36. package/dist/utils/parse-incomplete-markdown.js +173 -69
  37. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { useStreamdown } from '../context.svelte.js';
3
3
  import { scale } from 'svelte/transition';
4
- import { checkIcon, copyIcon, downloadIcon } from './icons.js';
4
+ import { checkIcon, closeIcon, copyIcon, downloadIcon, fullscreenIcon } from './icons.js';
5
5
  import { Popover } from './popover.svelte.js';
6
6
  import { useClickOutside } from '../utils/useClickOutside.svelte.js';
7
7
  import { useKeyDown } from '../utils/useKeyDown.svelte.js';
8
+ import { useExpand } from '../utils/expand.svelte.js';
8
9
  import type { TableToken } from '../marked/marked-table.js';
9
10
  import { useCopy } from '../utils/copy.svelte.js';
10
11
  import { save } from '../utils/save.js';
@@ -15,10 +16,13 @@
15
16
 
16
17
  let {
17
18
  token,
18
- id
19
+ id,
20
+ expanded = $bindable(false)
19
21
  }: {
20
22
  token: TableToken;
21
23
  id: string;
24
+ /** Owned by the caller: the wrapper it renders is what expands. */
25
+ expanded?: boolean;
22
26
  } = $props();
23
27
  const streamdown = useStreamdown();
24
28
  const popover = new Popover();
@@ -76,7 +80,22 @@
76
80
  }
77
81
  };
78
82
 
79
- const tableElement = () => document.querySelector(`[data-streamdown-table="${id}"]`);
83
+ const tableElement = () => document.querySelector<HTMLElement>(`[data-streamdown-table="${id}"]`);
84
+
85
+ const expand = useExpand({
86
+ get expanded() {
87
+ return expanded;
88
+ },
89
+ set expanded(value: boolean) {
90
+ expanded = value;
91
+ },
92
+ getTarget: tableElement
93
+ });
94
+ const expandLabel = $derived(
95
+ expanded
96
+ ? streamdown.translations.controls.exitTableFullscreen
97
+ : streamdown.translations.controls.tableFullscreen
98
+ );
80
99
 
81
100
  const copyOrDownload = (type: Format) => {
82
101
  if (type === 'Markdown') {
@@ -145,9 +164,15 @@
145
164
  </dialog>
146
165
  {/if}
147
166
 
167
+ <!-- The wrapper this toolbar controls becomes `position: fixed` when expanded,
168
+ so the buttons have to leave the flow with it or they end up underneath. -->
148
169
  <div
149
170
  data-streamdown-table-download
150
171
  class=" right-0 ml-auto flex items-center justify-end gap-2 p-1"
172
+ style:position={expanded ? 'fixed' : undefined}
173
+ style:top={expanded ? '24px' : undefined}
174
+ style:right={expanded ? '32px' : undefined}
175
+ style:z-index={expanded ? 2147483647 : undefined}
151
176
  >
152
177
  {#each modes as mode (mode)}
153
178
  <button
@@ -186,6 +211,22 @@
186
211
  {/if}
187
212
  </button>
188
213
  {/each}
214
+ {#if streamdown.controls.tableFullscreen}
215
+ <button
216
+ class={streamdown.theme.components.button}
217
+ onclick={(e) => expand.toggle(e.currentTarget)}
218
+ type="button"
219
+ title={expandLabel}
220
+ aria-label={expandLabel}
221
+ aria-pressed={expanded}
222
+ >
223
+ {#if expanded}
224
+ {@render (streamdown.icons?.close || closeIcon)()}
225
+ {:else}
226
+ {@render (streamdown.icons?.fullscreen || fullscreenIcon)()}
227
+ {/if}
228
+ </button>
229
+ {/if}
189
230
  <span aria-live="polite" style={srOnly}
190
231
  >{copy.isCopied ? streamdown.translations.controls.copiedTable : ''}</span
191
232
  >
@@ -2,7 +2,9 @@ import type { TableToken } from '../marked/marked-table.js';
2
2
  type $$ComponentProps = {
3
3
  token: TableToken;
4
4
  id: string;
5
+ /** Owned by the caller: the wrapper it renders is what expands. */
6
+ expanded?: boolean;
5
7
  };
6
- declare const TableDownload: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ declare const TableDownload: import("svelte").Component<$$ComponentProps, {}, "expanded">;
7
9
  type TableDownload = ReturnType<typeof TableDownload>;
8
10
  export default TableDownload;
@@ -1,17 +1,20 @@
1
1
  <script lang="ts">
2
2
  import { useStreamdown } from '../../context.svelte.js';
3
3
  import { usePinnedScroll } from '../../utils/usePinnedScroll.svelte.js';
4
- import type { Tokens } from 'marked';
4
+ import { resolveLineNumbers } from '../../utils/line-numbers.js';
5
+ import type { CodeToken } from '../../marked/index.js';
5
6
 
6
7
  const {
7
8
  token,
8
9
  id,
9
- incomplete = false
10
+ incomplete = false,
11
+ animate = true
10
12
  }: {
11
- token: Tokens.Code;
13
+ token: CodeToken;
12
14
  id: string;
13
15
  /** The fence is still being streamed; nothing below it is final yet. */
14
16
  incomplete?: boolean;
17
+ animate?: boolean;
15
18
  } = $props();
16
19
 
17
20
  const streamdown = useStreamdown();
@@ -22,6 +25,8 @@
22
25
  // too.
23
26
  const code = $derived(token.text.replace(/\n+$/, ''));
24
27
 
28
+ const lineNumbers = $derived(resolveLineNumbers(token.meta, streamdown.lineNumbers ?? false));
29
+
25
30
  // Same scroll container as Code.svelte: `pre` already scrolls horizontally.
26
31
  const pinnedScroll = usePinnedScroll({
27
32
  get maxHeight() {
@@ -36,7 +41,7 @@
36
41
  <div
37
42
  data-streamdown-code={id}
38
43
  data-incomplete={incomplete || undefined}
39
- style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
44
+ style={animate && streamdown.isMounted ? streamdown.animationBlockStyle : ''}
40
45
  class={streamdown.theme.code.base}
41
46
  >
42
47
  <div class={streamdown.theme.code.header}>
@@ -45,11 +50,16 @@
45
50
  <div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
46
51
  <pre
47
52
  class={streamdown.theme.code.pre}
53
+ data-line-numbers={lineNumbers.enabled || undefined}
54
+ style:counter-reset={lineNumbers.enabled ? `sd-line ${lineNumbers.start - 1}` : undefined}
48
55
  style:max-height={streamdown.codeBlockMaxHeight}
49
56
  style:overflow-y={streamdown.codeBlockMaxHeight ? 'auto' : undefined}
50
57
  {@attach pinnedScroll}><code
51
- >{#each code.split('\n') as line}<span class={streamdown.theme.code.line}
52
- ><span style={streamdown.isMounted ? streamdown.animationTextStyle : ''}
58
+ >{#each code.split('\n') as line}<span
59
+ class="{streamdown.theme.code.line}{lineNumbers.enabled
60
+ ? ` ${streamdown.theme.code.lineNumber}`
61
+ : ''}"
62
+ ><span style={animate && streamdown.isMounted ? streamdown.animationTextStyle : ''}
53
63
  >{line.trim().length > 0 ? line : '\u200B'}</span
54
64
  ></span
55
65
  >{/each}</code
@@ -1,9 +1,10 @@
1
- import type { Tokens } from 'marked';
1
+ import type { CodeToken } from '../../marked/index.js';
2
2
  type $$ComponentProps = {
3
- token: Tokens.Code;
3
+ token: CodeToken;
4
4
  id: string;
5
5
  /** The fence is still being streamed; nothing below it is final yet. */
6
6
  incomplete?: boolean;
7
+ animate?: boolean;
7
8
  };
8
9
  declare const CodeFallback: import("svelte").Component<$$ComponentProps, {}, "">;
9
10
  type CodeFallback = ReturnType<typeof CodeFallback>;
@@ -5,5 +5,6 @@ export declare const zoomInIcon: import("svelte").Snippet<[]>;
5
5
  export declare const zoomOutIcon: import("svelte").Snippet<[]>;
6
6
  export declare const fitViewIcon: import("svelte").Snippet<[]>;
7
7
  export declare const fullscreenIcon: import("svelte").Snippet<[]>;
8
+ export declare const closeIcon: import("svelte").Snippet<[]>;
8
9
  export declare const chevronRight: import("svelte").Snippet<[]>;
9
10
  export declare const chevronLeft: import("svelte").Snippet<[]>;
@@ -142,6 +142,14 @@ export const fullscreenIcon = createRawSnippet(() => {
142
142
  `
143
143
  };
144
144
  });
145
+ export const closeIcon = createRawSnippet(() => {
146
+ return {
147
+ render: () => `
148
+ <svg
149
+ aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
150
+ `
151
+ };
152
+ });
145
153
  export const chevronRight = createRawSnippet(() => {
146
154
  return {
147
155
  render: () => `
@@ -6,7 +6,13 @@
6
6
  type StreamdownProps
7
7
  } from './context.svelte.js';
8
8
  import { mergeTheme, shadcnTheme } from './theme.js';
9
- import { parseBlocks, createParseBlocksCache } from './marked/index.js';
9
+ import {
10
+ parseBlocks,
11
+ createParseBlocksCache,
12
+ compileTags,
13
+ DEFAULT_TAGS
14
+ } from './marked/index.js';
15
+ import { normalizeHtmlIndentation as dedentHtml } from './utils/normalize-html-indentation.js';
10
16
  import { hasUnclosedFence } from './utils/fence.js';
11
17
 
12
18
  let {
@@ -30,6 +36,7 @@
30
36
  controls,
31
37
  codeBlockMaxHeight,
32
38
  tableMaxHeight,
39
+ lineNumbers = false,
33
40
  animation,
34
41
  element = $bindable(),
35
42
  icons,
@@ -38,6 +45,9 @@
38
45
  sources,
39
46
  inlineCitationsMode = 'carousel',
40
47
  mdxComponents,
48
+ customTags,
49
+ literalTagContent,
50
+ normalizeHtmlIndentation = false,
41
51
  components,
42
52
  static: isStatic,
43
53
  ...snippets
@@ -55,6 +65,24 @@
55
65
  mermaidConfig?.theme ? mermaidConfig.theme : darkMode.current ? 'dark' : 'default'
56
66
  );
57
67
 
68
+ // Per-instance incremental state: append-only content updates re-lex only
69
+ // the last couple of blocks instead of the whole document.
70
+ const blocksCache = createParseBlocksCache();
71
+
72
+ // One compile per allowlist identity, shared by the lexer and the streaming
73
+ // completer so the two can never disagree about what counts as a tag.
74
+ const tags = $derived.by(() => {
75
+ const names = [...(customTags ?? []), ...Object.keys(mdxComponents ?? {})];
76
+ // With nothing to compile, share DEFAULT_TAGS: the lexer options cache is
77
+ // keyed on this object, so a private copy per instance would rebuild and
78
+ // retain its own ~20 tokenizer registrations for no reason.
79
+ return names.length === 0 && !literalTagContent?.length
80
+ ? DEFAULT_TAGS
81
+ : compileTags(names, literalTagContent);
82
+ });
83
+
84
+ const source = $derived(normalizeHtmlIndentation ? dedentHtml(content) : content);
85
+
58
86
  streamdown = new StreamdownContext({
59
87
  get element() {
60
88
  return element;
@@ -77,6 +105,12 @@
77
105
  get highlightTheme() {
78
106
  return resolvedHighlightTheme;
79
107
  },
108
+ get animateUpdate() {
109
+ // A streamed chunk keeps animating; a replacement or a bulk append (the
110
+ // demo's "Show All" is an append of 95% of the document) renders plain.
111
+ // The first parse stays animatable so `animateOnMount` keeps its meaning.
112
+ return blocksCache.lastUpdate !== 'bulk';
113
+ },
80
114
  get snippets() {
81
115
  return snippets;
82
116
  },
@@ -166,6 +200,7 @@
166
200
  tableCopy: table.copy,
167
201
  tableDownload: table.download,
168
202
  tableDownloadFilename: table.filename ?? 'table',
203
+ tableFullscreen: table.enabled && tableSection.fullscreen !== false,
169
204
  tableCsvSeparator: tableSection.csvSeparator ?? ',',
170
205
  mermaid: mermaid.enabled,
171
206
  mermaidDownload: mermaid.download,
@@ -178,6 +213,9 @@
178
213
  get codeBlockMaxHeight() {
179
214
  return codeBlockMaxHeight;
180
215
  },
216
+ get lineNumbers() {
217
+ return lineNumbers;
218
+ },
181
219
  get tableMaxHeight() {
182
220
  return tableMaxHeight;
183
221
  },
@@ -193,6 +231,9 @@
193
231
  get mdxComponents() {
194
232
  return mdxComponents;
195
233
  },
234
+ get tags() {
235
+ return tags;
236
+ },
196
237
  get components() {
197
238
  return components;
198
239
  }
@@ -200,11 +241,8 @@
200
241
 
201
242
  const id = $props.id();
202
243
 
203
- // Per-instance incremental state: append-only content updates re-lex only
204
- // the last couple of blocks instead of the whole document.
205
- const blocksCache = createParseBlocksCache();
206
244
  const blocks = $derived(
207
- isStatic ? content : parseBlocks(content, streamdown.extensions, blocksCache)
245
+ isStatic ? source : parseBlocks(source, streamdown.extensions, blocksCache, tags)
208
246
  );
209
247
 
210
248
  // Only the tail of a live stream can be mid-fence — a static render is finished
@@ -216,12 +254,13 @@
216
254
 
217
255
  <div bind:this={element} class={className}>
218
256
  {#if isStatic}
219
- <Block static={isStatic} block={content} />
257
+ <Block static={isStatic} block={source} />
220
258
  {:else}
221
259
  {#each blocks as block, index (`${id}-block-${index}`)}
222
260
  <Block
223
261
  static={isStatic}
224
262
  {block}
263
+ live={index === blocks.length - 1}
225
264
  incomplete={lastBlockIncomplete && index === blocks.length - 1}
226
265
  />
227
266
  {/each}
@@ -230,6 +269,29 @@
230
269
 
231
270
  <style global>
232
271
  :global {
272
+ /* Line numbers: a counter on the line span that already exists (why it is
273
+ here and not in the theme: see resolveLineNumbers). CSS ships as a string
274
+ at runtime, so keep this short. */
275
+ [data-streamdown-code] pre[data-line-numbers] > code > span {
276
+ counter-increment: sd-line;
277
+ }
278
+
279
+ [data-streamdown-code] pre[data-line-numbers] > code > span::before {
280
+ content: counter(sd-line);
281
+ }
282
+
283
+ /* Lives here, not in Mermaid.svelte: the table overlay needs it on pages
284
+ that never render a diagram. */
285
+ [data-expanded='true'] {
286
+ position: fixed;
287
+ top: 16px;
288
+ left: 16px;
289
+ width: calc(100vw - 32px);
290
+ height: calc(100vh - 32px);
291
+ z-index: 2147483647;
292
+ margin: 0px;
293
+ }
294
+
233
295
  @keyframes sd-fade {
234
296
  from {
235
297
  opacity: 0;
@@ -28,6 +28,9 @@ export type Translations = {
28
28
  tableFormatHtml: string;
29
29
  tableFormatCsv: string;
30
30
  tableFormatTsv: string;
31
+ tableFullscreen: string;
32
+ exitTableFullscreen: string;
33
+ table: string;
31
34
  downloadDiagram: string;
32
35
  downloadDiagramPng: string;
33
36
  downloadDiagramSvg: string;
@@ -48,8 +51,12 @@ export type Translations = {
48
51
  };
49
52
  export declare const defaultTranslations: Translations;
50
53
  export interface StreamdownContext extends Omit<StreamdownProps, keyof Snippets | 'class' | 'theme' | 'highlightTheme' | 'inlineCitationsMode'> {
54
+ /** `customTags` + `mdxComponents` keys compiled once; lexer and completer share it. */
55
+ tags: TagMatchers;
51
56
  snippets: Snippets;
52
57
  highlightTheme: HighlightTheme;
58
+ /** False while rendering a bulk update (a replacement or a paste-sized append); Block captures it per update. */
59
+ animateUpdate: boolean;
53
60
  theme: Theme;
54
61
  translations: Translations;
55
62
  controls: ResolvedControls;
@@ -69,10 +76,12 @@ export declare class StreamdownContext<Source extends Record<string, any> = Reco
69
76
  constructor(props: Omit<StreamdownProps, keyof Snippets | 'class' | 'highlightTheme'> & {
70
77
  snippets: Snippets<Source>;
71
78
  highlightTheme: HighlightTheme;
79
+ animateUpdate: boolean;
80
+ tags: TagMatchers;
72
81
  });
73
82
  }
74
83
  export declare const useStreamdown: () => StreamdownContext<Record<string, any>>;
75
- import type { AlertToken, CodeToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken, CitationToken, MdxToken } from './marked/index.js';
84
+ import type { AlertToken, CodeToken, MathToken, SubSupToken, TableToken, THead, TBody, TFoot, THeadRow, TRow, TD, TH, Extension, GenericToken, CitationToken, MdxToken, TagMatchers } from './marked/index.js';
76
85
  import type { Tokens } from 'marked';
77
86
  import type { CsvSeparator } from './utils/table-export.js';
78
87
  import type { ListItemToken, ListToken } from './marked/marked-list.js';
@@ -148,6 +157,7 @@ export type TableControls = boolean | {
148
157
  enabled?: boolean;
149
158
  copy?: boolean;
150
159
  download?: DownloadControl<TableToken>;
160
+ fullscreen?: boolean;
151
161
  csvSeparator?: CsvSeparator;
152
162
  };
153
163
  export type MermaidControls = boolean | {
@@ -170,6 +180,7 @@ export type ResolvedControls = {
170
180
  tableCopy: boolean;
171
181
  tableDownload: boolean;
172
182
  tableDownloadFilename: string | ((token: TableToken) => string);
183
+ tableFullscreen: boolean;
173
184
  tableCsvSeparator: CsvSeparator;
174
185
  mermaid: boolean;
175
186
  mermaidDownload: boolean;
@@ -203,6 +214,13 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
203
214
  [K in keyof Translations]?: Partial<Translations[K]>;
204
215
  };
205
216
  controls?: Controls;
217
+ /**
218
+ * Number the lines of every code block. Default false — upstream defaults it on,
219
+ * but turning it on here would change every existing render. A fence can flip it
220
+ * either way with a `lineNumbers` / `noLineNumbers` meta word, and start at N with
221
+ * `startLine=N`.
222
+ */
223
+ lineNumbers?: boolean;
206
224
  codeBlockMaxHeight?: string;
207
225
  tableMaxHeight?: string;
208
226
  renderHtml?: boolean | ((token: Tokens.HTML | Tokens.Tag) => string);
@@ -218,6 +236,7 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
218
236
  copy?: Snippet;
219
237
  download?: Snippet;
220
238
  fullscreen?: Snippet;
239
+ close?: Snippet;
221
240
  zoomIn?: Snippet;
222
241
  zoomOut?: Snippet;
223
242
  fitView?: Snippet;
@@ -231,6 +250,21 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
231
250
  check?: Snippet;
232
251
  };
233
252
  extensions?: Extension[];
253
+ /**
254
+ * Extra tag names the MDX tokenizer accepts on top of PascalCase, so
255
+ * `<ai-thinking>**bold**</ai-thinking>` becomes a component with parsed
256
+ * children instead of a literal html block. Keys of `mdxComponents` are
257
+ * allowed automatically; nothing else lowercase is, so real HTML is untouched.
258
+ */
259
+ customTags?: string[];
260
+ /** Tags whose children render verbatim — no Markdown, `**` and `_` left alone. */
261
+ literalTagContent?: string[];
262
+ /**
263
+ * Dedent pretty-printed HTML before parsing, so a nested `<div>` indented four
264
+ * spaces after a blank line is not read as an indented code block. Off by
265
+ * default: dedenting is lossy, and `<pre>`/`<code>` bodies are never touched.
266
+ */
267
+ normalizeHtmlIndentation?: boolean;
234
268
  children?: Snippet<[{
235
269
  streamdown: StreamdownContext;
236
270
  token: GenericToken;
@@ -246,11 +280,13 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
246
280
  token: Tokens.Code;
247
281
  id: string;
248
282
  incomplete: boolean;
283
+ animate: boolean;
249
284
  }, any, any>;
250
285
  mermaid?: Component<{
251
286
  token: Tokens.Code;
252
287
  id: string;
253
288
  incomplete: boolean;
289
+ animate: boolean;
254
290
  }, any, any>;
255
291
  math?: Component<{
256
292
  token: MathToken;
@@ -19,6 +19,9 @@ export const defaultTranslations = {
19
19
  tableFormatHtml: 'HTML',
20
20
  tableFormatCsv: 'CSV',
21
21
  tableFormatTsv: 'TSV',
22
+ tableFullscreen: 'Expand table',
23
+ exitTableFullscreen: 'Collapse table',
24
+ table: 'Table',
22
25
  downloadDiagram: 'Download diagram',
23
26
  downloadDiagramPng: 'PNG',
24
27
  downloadDiagramSvg: 'SVG',
package/dist/index.d.ts CHANGED
@@ -2,7 +2,8 @@ export { default as Streamdown } from './Streamdown.svelte';
2
2
  export { useStreamdown, defaultTranslations, type StreamdownProps, type Translations, type Controls, type CodeControls, type TableControls, type MermaidControls, type ResolvedControls } from './context.svelte.js';
3
3
  export { extractTableData, tableDataToCSV, tableDataToTSV, tableDataToMarkdown, tableDataToHTML, type TableData, type CsvSeparator } from './utils/table-export.js';
4
4
  export { theme, shadcnTheme, mergeTheme, type Theme } from './theme.js';
5
- export { type CodeToken, type Extension, type StreamdownToken, lex, parseBlocks } from './marked/index.js';
6
- export { parseIncompleteMarkdown, type Plugin, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
5
+ export { type CodeToken, type Extension, type StreamdownToken, type TagMatchers, compileTags, lex, parseBlocks } from './marked/index.js';
6
+ export { normalizeHtmlIndentation } from './utils/normalize-html-indentation.js';
7
+ export { parseIncompleteMarkdown, type Plugin, type CompleterOptions, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
7
8
  export { defineLanguage, type LanguageDefinition } from '@tanstack/highlight';
8
9
  export type { HighlightTheme } from '@tanstack/highlight/theme';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { default as Streamdown } from './Streamdown.svelte';
2
2
  export { useStreamdown, defaultTranslations } from './context.svelte.js';
3
3
  export { extractTableData, tableDataToCSV, tableDataToTSV, tableDataToMarkdown, tableDataToHTML } from './utils/table-export.js';
4
4
  export { theme, shadcnTheme, mergeTheme } from './theme.js';
5
- export { lex, parseBlocks } from './marked/index.js';
5
+ export { compileTags, lex, parseBlocks } from './marked/index.js';
6
+ export { normalizeHtmlIndentation } from './utils/normalize-html-indentation.js';
6
7
  export { parseIncompleteMarkdown, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
7
8
  export { defineLanguage } from '@tanstack/highlight';
@@ -10,7 +10,7 @@ import { type TableToken, type THead, type TBody, type TFoot, type THeadRow, typ
10
10
  import { type DescriptionDetailToken, type DescriptionListToken, type DescriptionTermToken, type DescriptionToken } from './marked-dl.js';
11
11
  import { type AlignToken } from './marked-align.js';
12
12
  import { type CitationToken } from './marked-citations.js';
13
- import { type MdxToken } from './marked-mdx.js';
13
+ import { type MdxToken, type TagMatchers } from './marked-mdx.js';
14
14
  export type GenericToken = {
15
15
  type: string;
16
16
  raw: string;
@@ -32,7 +32,7 @@ export type CodeToken = Tokens.Code & {
32
32
  };
33
33
  export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem | Tokens.Table | Tokens.Code> | CodeToken | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | BrToken | HrToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD | DescriptionListToken | DescriptionToken | DescriptionDetailToken | DescriptionTermToken | AlignToken | CitationToken | MdxToken;
34
34
  export type { TableToken, THead, TBody, TFoot, THeadRow, TRow, TH, TD } from './marked-table.js';
35
- export declare const lex: (markdown: string, extensions?: Extension[]) => StreamdownToken[];
35
+ export declare const lex: (markdown: string, extensions?: Extension[], tags?: TagMatchers) => StreamdownToken[];
36
36
  /**
37
37
  * Opaque incremental state for `parseBlocks`. Create one per Streamdown
38
38
  * instance (or per simulated stream) and pass it on every call: append-only
@@ -57,7 +57,14 @@ export type ParseBlocksCache = {
57
57
  keptBefore: number[];
58
58
  /** persistent list of kept raws; only its tail is rewritten on append */
59
59
  blocks: string[];
60
+ /**
61
+ * What kind of update the last call was. Rendering skips the streaming
62
+ * animation on a `bulk` one — a replacement, or an append far larger than any
63
+ * streamed chunk — where ~10k spans would otherwise all animate in one frame.
64
+ */
65
+ lastUpdate: 'first' | 'stream' | 'bulk';
60
66
  };
61
67
  export declare const createParseBlocksCache: () => ParseBlocksCache;
62
- export declare const parseBlocks: (markdown: string, extensions?: Extension[], cache?: ParseBlocksCache) => string[];
63
- export type { MathToken, AlertToken, FootnoteToken, SubSupToken, BrToken, HrToken, AlignToken, CitationToken, MdxToken };
68
+ export declare const parseBlocks: (markdown: string, extensions?: Extension[], cache?: ParseBlocksCache, tags?: TagMatchers) => string[];
69
+ export { compileTags, DEFAULT_TAGS } from './marked-mdx.js';
70
+ export type { TagMatchers, MathToken, AlertToken, FootnoteToken, SubSupToken, BrToken, HrToken, AlignToken, CitationToken, MdxToken };