svelte-streamdown 2.3.3 → 2.4.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/dist/Block.svelte CHANGED
@@ -4,7 +4,6 @@
4
4
  import { lex, type StreamdownToken } from './marked/index.js';
5
5
  import AnimatedText from './AnimatedText.svelte';
6
6
  import { useStreamdown } from './context.svelte.js';
7
- import AnimatedBlock from './AnimatedBlock.svelte';
8
7
 
9
8
  let {
10
9
  block,
@@ -38,10 +37,4 @@
38
37
  {/each}
39
38
  {/snippet}
40
39
 
41
- {#if streamdown.animation.enabled && !insideFootnote}
42
- <AnimatedBlock>
43
- {@render renderChildren(tokens)}
44
- </AnimatedBlock>
45
- {:else}
46
- {@render renderChildren(tokens)}
47
- {/if}
40
+ {@render renderChildren(tokens)}
@@ -36,7 +36,10 @@
36
36
  }}
37
37
  render={streamdown.snippets.alert}
38
38
  >
39
- <div class={`${streamdown.theme.alert.base} ${streamdown.theme.alert[token.variant]}`}>
39
+ <div
40
+ style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
41
+ class={`${streamdown.theme.alert.base} ${streamdown.theme.alert[token.variant]}`}
42
+ >
40
43
  <div data-alert-title class={streamdown.theme.alert.title}>
41
44
  {@render (streamdown.icons?.[token.variant] || icon)()}
42
45
  {streamdown.translations?.alert?.[token.variant] || token.variant}
@@ -14,7 +14,8 @@
14
14
  } = $props();
15
15
 
16
16
  const streamdown = useStreamdown();
17
- const highlighter = HighlighterManager.create(streamdown.shikiPreloadThemes || []);
17
+ const highlighter = HighlighterManager.create(streamdown.shikiPreloadThemes);
18
+
18
19
  const copy = useCopy({
19
20
  get content() {
20
21
  return token.text;
@@ -44,11 +45,12 @@
44
45
  void highlighter.load(theme, lang);
45
46
  });
46
47
  });
47
-
48
- const isMounted = streamdown.isMounted;
49
48
  </script>
50
49
 
51
- <div class={streamdown.theme.code.base}>
50
+ <div
51
+ style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
52
+ class={streamdown.theme.code.base}
53
+ >
52
54
  <div class={streamdown.theme.code.header}>
53
55
  <span class={streamdown.theme.code.language}>{token.lang}</span>
54
56
  {#if streamdown.controls.code}
@@ -84,7 +86,7 @@
84
86
  <span class={streamdown.theme.code.line}>
85
87
  {#each line as token}
86
88
  <span
87
- style={isMounted ? streamdown.animationTextStyle : undefined}
89
+ style={streamdown.animationTextStyle}
88
90
  style:color={token.color}
89
91
  style:background-color={token.bgColor}
90
92
  >
@@ -97,7 +99,7 @@
97
99
 
98
100
  {#snippet Skeleton(lines: string[])}
99
101
  {#each lines as line}
100
- <span class={streamdown.theme.code.skeleton}>
102
+ <span style={streamdown.animationTextStyle} class={streamdown.theme.code.skeleton}>
101
103
  {line.trim().length > 0 ? line : '\u200B'}
102
104
  </span>
103
105
  {/each}
@@ -12,6 +12,9 @@
12
12
  import FootnoteRef from './FootnoteRef.svelte';
13
13
  let { token, children }: { token: StreamdownToken; children: Snippet } = $props();
14
14
  const streamdown = useStreamdown();
15
+
16
+ // Only apply animation on block level elements. Leaves text elements to be animated by their text children.
17
+ const style = $derived(streamdown.isMounted ? streamdown.animationBlockStyle : '');
15
18
  </script>
16
19
 
17
20
  {#if token.type === 'heading'}
@@ -23,40 +26,40 @@
23
26
  render={streamdown.snippets.heading}
24
27
  >
25
28
  {#if token.depth === 1}
26
- <h1 class={streamdown.theme[`h${token.depth}`].base}>
29
+ <h1 {style} class={streamdown.theme[`h${token.depth}`].base}>
27
30
  {@render children()}
28
31
  </h1>
29
32
  {:else if token.depth === 2}
30
- <h2 class={streamdown.theme[`h${token.depth}`].base}>
33
+ <h2 {style} class={streamdown.theme[`h${token.depth}`].base}>
31
34
  {@render children()}
32
35
  </h2>
33
36
  {:else if token.depth === 3}
34
- <h3 class={streamdown.theme[`h${token.depth}`].base}>
37
+ <h3 {style} class={streamdown.theme[`h${token.depth}`].base}>
35
38
  {@render children()}
36
39
  </h3>
37
40
  {:else if token.depth === 4}
38
- <h4 class={streamdown.theme[`h${token.depth}`].base}>
41
+ <h4 {style} class={streamdown.theme[`h${token.depth}`].base}>
39
42
  {@render children()}
40
43
  </h4>
41
44
  {:else if token.depth === 5}
42
- <h5 class={streamdown.theme[`h${token.depth}`].base}>
45
+ <h5 {style} class={streamdown.theme[`h${token.depth}`].base}>
43
46
  {@render children()}
44
47
  </h5>
45
48
  {:else if token.depth === 6}
46
- <h6 class={streamdown.theme[`h${token.depth}`].base}>
49
+ <h6 {style} class={streamdown.theme[`h${token.depth}`].base}>
47
50
  {@render children()}
48
51
  </h6>
49
52
  {/if}
50
53
  </Slot>
51
54
  {:else if token.type === 'paragraph'}
52
55
  <Slot props={{ children, token }} render={streamdown.snippets.paragraph}>
53
- <p class={streamdown.theme.paragraph.base}>
56
+ <p {style} class={streamdown.theme.paragraph.base}>
54
57
  {@render children()}
55
58
  </p>
56
59
  </Slot>
57
60
  {:else if token.type === 'blockquote'}
58
61
  <Slot props={{ children, token }} render={streamdown.snippets.blockquote}>
59
- <blockquote class={streamdown.theme.blockquote.base}>
62
+ <blockquote {style} class={streamdown.theme.blockquote.base}>
60
63
  {@render children()}
61
64
  </blockquote>
62
65
  </Slot>
@@ -91,7 +94,7 @@
91
94
  {:else if token.type === 'list_item'}
92
95
  <Slot props={{ children, token }} render={streamdown.snippets.li}>
93
96
  <li
94
- style={streamdown.isMounted ? streamdown.animationBlockStyle : undefined}
97
+ {style}
95
98
  style:list-style-type={token.task ? 'none' : undefined}
96
99
  {...token.value && !token.task ? { value: token.value } : {}}
97
100
  class={streamdown.theme.li.base}
@@ -109,7 +112,7 @@
109
112
  </Slot>
110
113
  {:else if token.type === 'table'}
111
114
  <Slot props={{ token, children }} render={streamdown.snippets.table}>
112
- <div class={streamdown.theme.table.base}>
115
+ <div {style} class={streamdown.theme.table.base}>
113
116
  <table class={streamdown.theme.table.table}>
114
117
  {@render children()}
115
118
  </table>
@@ -117,25 +120,25 @@
117
120
  </Slot>
118
121
  {:else if token.type === 'thead'}
119
122
  <Slot props={{ token, children }} render={streamdown.snippets.thead}>
120
- <thead class={streamdown.theme.thead.base}>
123
+ <thead {style} class={streamdown.theme.thead.base}>
121
124
  {@render children?.()}
122
125
  </thead>
123
126
  </Slot>
124
127
  {:else if token.type === 'tbody'}
125
128
  <Slot props={{ token, children }} render={streamdown.snippets.tbody}>
126
- <tbody class={streamdown.theme.tbody.base}>
129
+ <tbody {style} class={streamdown.theme.tbody.base}>
127
130
  {@render children?.()}
128
131
  </tbody>
129
132
  </Slot>
130
133
  {:else if token.type === 'tfoot'}
131
134
  <Slot props={{ token, children }} render={streamdown.snippets.tfoot}>
132
- <tfoot class={streamdown.theme.tfoot.base}>
135
+ <tfoot {style} class={streamdown.theme.tfoot.base}>
133
136
  {@render children?.()}
134
137
  </tfoot>
135
138
  </Slot>
136
139
  {:else if token.type === 'tr'}
137
140
  <Slot props={{ token, children }} render={streamdown.snippets.tr}>
138
- <tr class={streamdown.theme.tr.base}>
141
+ <tr {style} class={streamdown.theme.tr.base}>
139
142
  {@render children?.()}
140
143
  </tr>
141
144
  </Slot>
@@ -143,6 +146,7 @@
143
146
  {#if token.rowspan > 0}
144
147
  <Slot props={{ children, token }} render={streamdown.snippets.td}>
145
148
  <td
149
+ {style}
146
150
  class={streamdown.theme.td.base}
147
151
  {...token.colspan > 1 ? { colspan: token.colspan } : {}}
148
152
  {...token.rowspan > 1 ? { rowspan: token.rowspan } : {}}
@@ -158,6 +162,7 @@
158
162
  {#if token.rowspan > 0}
159
163
  <Slot props={{ children, token }} render={streamdown.snippets.th}>
160
164
  <th
165
+ {style}
161
166
  class={streamdown.theme.th.base}
162
167
  {...token.colspan > 1 ? { colspan: token.colspan } : {}}
163
168
  {...token.rowspan > 1 ? { rowspan: token.rowspan } : {}}
@@ -205,7 +210,7 @@
205
210
  </Slot>
206
211
  {:else if token.type === 'hr'}
207
212
  <Slot props={{ children, token }} render={streamdown.snippets.hr}>
208
- <hr class={streamdown.theme.hr.base} />
213
+ <hr {style} class={streamdown.theme.hr.base} />
209
214
  </Slot>
210
215
  {:else if token.type === 'br'}
211
216
  <br />
@@ -227,25 +232,23 @@
227
232
  <!-- TODO Footnotes are rendered inside the FootnoteRef popover -->
228
233
  {:else if token.type === 'descriptionList'}
229
234
  <Slot props={{ children, token }} render={streamdown.snippets.descriptionList}>
230
- <dl class={streamdown.theme.descriptionList.base}>
235
+ <dl style={streamdown.animationBlockStyle} class={streamdown.theme.descriptionList.base}>
231
236
  {@render children()}
232
237
  </dl>
233
238
  </Slot>
234
239
  {:else if token.type === 'description'}
235
240
  <Slot props={{ children, token }} render={streamdown.snippets.description}>
236
- <div class={streamdown.theme.description.base}>
237
- {@render children()}
238
- </div>
241
+ {@render children()}
239
242
  </Slot>
240
243
  {:else if token.type === 'descriptionTerm'}
241
244
  <Slot props={{ children, token }} render={streamdown.snippets.descriptionTerm}>
242
- <dt class={streamdown.theme.descriptionTerm.base}>
245
+ <dt {style} class={streamdown.theme.descriptionTerm.base}>
243
246
  {@render children()}
244
247
  </dt>
245
248
  </Slot>
246
249
  {:else if token.type === 'descriptionDetail'}
247
250
  <Slot props={{ children, token }} render={streamdown.snippets.descriptionDetail}>
248
- <dd class={streamdown.theme.descriptionDetail.base}>
251
+ <dd {style} class={streamdown.theme.descriptionDetail.base}>
249
252
  {@render children()}
250
253
  </dd>
251
254
  </Slot>
@@ -90,12 +90,14 @@
90
90
  render={streamdown.snippets.footnotePopover}
91
91
  >
92
92
  <dialog
93
+ style="z-index: 1000; position: fixed;"
93
94
  id={'footnote-popover-' + id}
94
95
  aria-modal="false"
95
96
  transition:scale|global={{ start: 0.95, duration: 100 }}
96
97
  {@attach clickOutside.attachment}
97
98
  {@attach popoverAttachment}
98
99
  open
100
+ popover="auto"
99
101
  class={`${streamdown.theme.footnotePopover.base}`}
100
102
  >
101
103
  {#each token.content.lines as line}
@@ -112,7 +114,7 @@
112
114
  render={streamdown.snippets.footnoteRef}
113
115
  >
114
116
  <button
115
- style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
117
+ style={streamdown.animationBlockStyle}
116
118
  bind:this={reference}
117
119
  class={streamdown.theme.footnoteRef.base}
118
120
  onclick={() => (isOpen = !isOpen)}
@@ -31,7 +31,10 @@
31
31
  }}
32
32
  render={streamdown.snippets.image}
33
33
  >
34
- <span class={streamdown.theme.image.base}>
34
+ <span
35
+ style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
36
+ class={streamdown.theme.image.base}
37
+ >
35
38
  <img class={streamdown.theme.image.image} src={transformedUrl} alt={token.text} />
36
39
  </span>
37
40
  </Slot>
@@ -53,7 +53,7 @@
53
53
  {@html html}
54
54
  </span>
55
55
  {:else}
56
- <div class="h-fit w-full">
56
+ <div style={streamdown.isMounted ? streamdown.animationBlockStyle : ''} class="h-fit w-full">
57
57
  <div class="overflow-x-auto">
58
58
  <div bind:this={inner} class={streamdown.theme.math.block}>
59
59
  {@html html}
@@ -202,6 +202,7 @@
202
202
 
203
203
  {#if mermaid}
204
204
  <div
205
+ style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
205
206
  class={streamdown.theme.mermaid.base}
206
207
  {@attach (node) => renderMermaid(token.text, node)}
207
208
  {@attach insider.attach}
package/dist/theme.d.ts CHANGED
@@ -131,9 +131,6 @@ export declare const theme: {
131
131
  descriptionList: {
132
132
  base: string;
133
133
  };
134
- description: {
135
- base: string;
136
- };
137
134
  descriptionTerm: {
138
135
  base: string;
139
136
  };
@@ -272,9 +269,6 @@ export declare const shadcnTheme: {
272
269
  descriptionList: {
273
270
  base: string;
274
271
  };
275
- description: {
276
- base: string;
277
- };
278
272
  descriptionTerm: {
279
273
  base: string;
280
274
  };
@@ -418,9 +412,6 @@ export declare const mergeTheme: (customTheme?: DeepPartialTheme, baseTheme?: "t
418
412
  descriptionList: {
419
413
  base: string;
420
414
  };
421
- description: {
422
- base: string;
423
- };
424
415
  descriptionTerm: {
425
416
  base: string;
426
417
  };
package/dist/theme.js CHANGED
@@ -132,11 +132,8 @@ export const theme = {
132
132
  descriptionList: {
133
133
  base: 'my-4 space-y-2'
134
134
  },
135
- description: {
136
- base: 'border-l-2 border-gray-200 pl-4'
137
- },
138
135
  descriptionTerm: {
139
- base: 'font-semibold text-gray-900'
136
+ base: 'font-semibold text-gray-900 border-l-2 border-gray-200 pl-4'
140
137
  },
141
138
  descriptionDetail: {
142
139
  base: 'text-gray-700 ml-4 leading-relaxed'
@@ -273,11 +270,8 @@ export const shadcnTheme = {
273
270
  descriptionList: {
274
271
  base: 'my-4 space-y-2'
275
272
  },
276
- description: {
277
- base: 'border-l-2 border-border pl-4'
278
- },
279
273
  descriptionTerm: {
280
- base: 'font-semibold text-foreground'
274
+ base: 'font-semibold text-foreground border-l-2 border-border pl-4'
281
275
  },
282
276
  descriptionDetail: {
283
277
  base: 'text-muted-foreground ml-4 leading-relaxed'
@@ -26,7 +26,7 @@ declare class HighlighterManager {
26
26
  * Highlights code synchronously. Must call isReady() first.
27
27
  */
28
28
  highlightCode(code: string, language: string | undefined, theme: BundledTheme): ThemedToken[][];
29
- static create(preloadedThemes: BundledTheme[]): HighlighterManager;
29
+ static create(preloadedThemes?: BundledTheme[]): HighlighterManager;
30
30
  }
31
31
  export { HighlighterManager };
32
32
  export declare const languageExtensionMap: Record<string, string>;
@@ -23,7 +23,7 @@ class HighlighterManager {
23
23
  if (this.highlighter instanceof Promise) {
24
24
  return this.highlighter;
25
25
  }
26
- else {
26
+ else if (!this.highlighter) {
27
27
  this.highlighter = new Promise((resolve, reject) => {
28
28
  loadShiki().then(([engine, createHighlighter]) => {
29
29
  return createHighlighter({
@@ -38,6 +38,9 @@ class HighlighterManager {
38
38
  });
39
39
  return this.highlighter;
40
40
  }
41
+ else {
42
+ return this.highlighter;
43
+ }
41
44
  }
42
45
  async loadTheme(theme, highlighter) {
43
46
  const themeLoader = this.loadedThemes.get(theme);
@@ -69,27 +72,17 @@ class HighlighterManager {
69
72
  return Object.hasOwn(bundledLanguages, language);
70
73
  };
71
74
  isReady(theme, language = 'bash') {
72
- return (!(this.highlighter instanceof Promise) &&
73
- this.loadedLanguages.get(this.isLanguageSupported(language) ? language : 'bash') === true &&
74
- this.loadedThemes.get(theme) === true);
75
+ return (!!this.highlighter &&
76
+ !(this.highlighter instanceof Promise) &&
77
+ this.loadedThemes.get(theme) === true &&
78
+ this.loadedLanguages.get(this.isLanguageSupported(language) ? language : 'bash') === true);
75
79
  }
76
80
  /**
77
81
  * Preloads themes by creating minimal highlighter instances.
78
82
  * This reduces flickering when switching themes.
79
83
  */
80
84
  async preloadThemes(highlighter) {
81
- for (const theme of this.preloadedThemes) {
82
- const themeLoader = this.loadedThemes.get(theme);
83
- if (themeLoader === true) {
84
- return;
85
- }
86
- else if (themeLoader instanceof Promise) {
87
- await themeLoader;
88
- }
89
- else {
90
- await this.loadTheme(theme, highlighter);
91
- }
92
- }
85
+ await Promise.all(Array.from(this.preloadedThemes).map((theme) => this.loadTheme(theme, highlighter)));
93
86
  }
94
87
  /**
95
88
  * Ensures the highlighter is ready for the given theme and language.
@@ -101,7 +94,6 @@ class HighlighterManager {
101
94
  !(this.highlighter instanceof Promise) &&
102
95
  themeLoader === true &&
103
96
  languageLoader === true) {
104
- // already loaded
105
97
  return;
106
98
  }
107
99
  const highlighter = await this.loadHighlighter();
@@ -131,7 +123,7 @@ class HighlighterManager {
131
123
  return [];
132
124
  }
133
125
  }
134
- static create(preloadedThemes) {
126
+ static create(preloadedThemes = []) {
135
127
  if (typeof window !== 'undefined' && 'STREAMDOWN_HIGHLIGHTER' in window) {
136
128
  const previousHighlighter = window.STREAMDOWN_HIGHLIGHTER;
137
129
  preloadedThemes.forEach((theme) => previousHighlighter.preloadedThemes.add(theme));
@@ -807,6 +807,67 @@ const handleIncompleteFootnotes = (text) => {
807
807
  }
808
808
  return result;
809
809
  };
810
+ // Completes incomplete description list items (:term -> :term:)
811
+ // Also processes incomplete inline formatting within the description content
812
+ const handleIncompleteDescriptionList = (text) => {
813
+ const lines = text.split('\n');
814
+ let inFence = false;
815
+ let fenceChar = '';
816
+ let fenceIndent = '';
817
+ for (let i = 0; i < lines.length; i++) {
818
+ const line = lines[i];
819
+ // Check for fence open/close
820
+ const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
821
+ if (fenceMatch) {
822
+ const indent = fenceMatch[1];
823
+ const fence = fenceMatch[2];
824
+ const lang = fenceMatch[3];
825
+ if (!inFence) {
826
+ // Opening fence
827
+ inFence = true;
828
+ fenceChar = fence.substring(0, 1); // Store just the first character
829
+ fenceIndent = indent;
830
+ continue;
831
+ }
832
+ else if (fence.startsWith(fenceChar) && indent === fenceIndent) {
833
+ // Closing fence - match the fence character type and indentation
834
+ inFence = false;
835
+ fenceChar = '';
836
+ fenceIndent = '';
837
+ continue;
838
+ }
839
+ }
840
+ // Skip processing if inside a fence
841
+ if (inFence) {
842
+ continue;
843
+ }
844
+ // Detect description-list lines with /^\s*:[^:]+$/
845
+ const descriptionMatch = line.match(/^(\s*:[^:]+)$/);
846
+ if (descriptionMatch) {
847
+ const fullMatch = descriptionMatch[1];
848
+ const colonIndex = fullMatch.indexOf(':');
849
+ const beforeColon = fullMatch.substring(0, colonIndex + 1);
850
+ const content = fullMatch.substring(colonIndex + 1);
851
+ // Apply formatters in this order
852
+ let processedContent = content;
853
+ processedContent = handleIncompleteBoldItalic(processedContent); // ***
854
+ processedContent = handleIncompleteBold(processedContent); // **
855
+ processedContent = handleIncompleteDoubleUnderscoreItalic(processedContent); // __
856
+ processedContent = handleIncompleteStrikethrough(processedContent); // ~~
857
+ processedContent = handleIncompleteInlineCode(processedContent); // `
858
+ processedContent = handleIncompleteSingleAsteriskItalic(processedContent); // *
859
+ processedContent = handleIncompleteSingleUnderscoreItalic(processedContent); // _
860
+ processedContent = handleIncompleteSub(processedContent); // ~
861
+ processedContent = handleIncompleteSup(processedContent); // ^
862
+ processedContent = handleIncompleteBlockKatex(processedContent);
863
+ processedContent = handleIncompleteInlineMath(processedContent);
864
+ processedContent = handleIncompleteLinksAndImages(processedContent);
865
+ // Set the line to beforeColon + processedContent + ':'
866
+ lines[i] = beforeColon + processedContent + ':';
867
+ }
868
+ }
869
+ return lines.join('\n');
870
+ };
810
871
  // Parses markdown text and removes incomplete tokens to prevent partial rendering
811
872
  export const parseIncompleteMarkdown = (text) => {
812
873
  if (!text || typeof text !== 'string') {
@@ -818,6 +879,8 @@ export const parseIncompleteMarkdown = (text) => {
818
879
  result = handleIncompleteCodeBlock(result);
819
880
  // Handle incomplete footnotes FIRST (before any other processing to avoid conflicts)
820
881
  result = handleIncompleteFootnotes(result);
882
+ // Handle incomplete description lists (block-level elements)
883
+ result = handleIncompleteDescriptionList(result);
821
884
  // Handle various formatting completions ONLY if not inside a code block
822
885
  // Handle double patterns before single patterns for proper priority
823
886
  result = handleIncompleteBoldItalic(result); // ***
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1,16 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { useStreamdown } from './context.svelte.js';
4
-
5
- let { children }: { children: Snippet } = $props();
6
-
7
- const streamdown = useStreamdown();
8
- </script>
9
-
10
- {#if streamdown.isMounted}
11
- <div style={streamdown.animationBlockStyle}>
12
- {@render children()}
13
- </div>
14
- {:else}
15
- {@render children()}
16
- {/if}
@@ -1,7 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- type $$ComponentProps = {
3
- children: Snippet;
4
- };
5
- declare const AnimatedBlock: import("svelte").Component<$$ComponentProps, {}, "">;
6
- type AnimatedBlock = ReturnType<typeof AnimatedBlock>;
7
- export default AnimatedBlock;