svelte-streamdown 2.6.1 → 3.0.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.
@@ -8,13 +8,14 @@
8
8
  content = '',
9
9
  class: className,
10
10
  shikiTheme,
11
- shikiPreloadThemes,
11
+ shikiLanguages,
12
+ shikiThemes,
12
13
  parseIncompleteMarkdown,
13
14
  defaultOrigin,
14
15
  allowedLinkPrefixes = ['*'],
15
16
  allowedImagePrefixes = ['*'],
16
17
  theme,
17
- mermaidConfig,
18
+ mermaidConfig = {},
18
19
  katexConfig,
19
20
  translations,
20
21
  baseTheme,
@@ -30,8 +31,25 @@
30
31
  sources,
31
32
  inlineCitationsMode = 'carousel',
32
33
  mdxComponents,
34
+ components,
35
+ static: isStatic,
33
36
  ...snippets
34
37
  }: StreamdownProps<Source> = $props();
38
+ import { useDarkMode } from './utils/darkMode.svelte.js';
39
+
40
+ const darkMode = useDarkMode();
41
+
42
+ const shikiThemedTheme = $derived(
43
+ shikiThemes
44
+ ? Object.keys(shikiThemes)[0] || 'github-light'
45
+ : darkMode.current
46
+ ? 'github-dark'
47
+ : 'github-light'
48
+ );
49
+
50
+ const mermaidThemedTheme = $derived(
51
+ mermaidConfig?.theme ? mermaidConfig.theme : darkMode.current ? 'dark' : 'default'
52
+ );
35
53
 
36
54
  streamdown = new StreamdownContext({
37
55
  get element() {
@@ -53,7 +71,7 @@
53
71
  return allowedImagePrefixes;
54
72
  },
55
73
  get shikiTheme() {
56
- return shikiTheme || 'github-light';
74
+ return shikiTheme || shikiThemedTheme;
57
75
  },
58
76
  get snippets() {
59
77
  return snippets;
@@ -67,7 +85,10 @@
67
85
  return baseTheme;
68
86
  },
69
87
  get mermaidConfig() {
70
- return mermaidConfig;
88
+ return {
89
+ theme: mermaidThemedTheme,
90
+ ...mermaidConfig
91
+ };
71
92
  },
72
93
  get katexConfig() {
73
94
  return katexConfig;
@@ -78,8 +99,11 @@
78
99
  get translations() {
79
100
  return translations;
80
101
  },
81
- get shikiPreloadThemes() {
82
- return shikiPreloadThemes;
102
+ get shikiLanguages() {
103
+ return shikiLanguages;
104
+ },
105
+ get shikiThemes() {
106
+ return shikiThemes;
83
107
  },
84
108
  get sources() {
85
109
  return sources;
@@ -122,18 +146,25 @@
122
146
  },
123
147
  get mdxComponents() {
124
148
  return mdxComponents;
149
+ },
150
+ get components() {
151
+ return components;
125
152
  }
126
153
  });
127
154
 
128
155
  const id = $props.id();
129
156
 
130
- const blocks = $derived(parseBlocks(content, streamdown.extensions));
157
+ const blocks = $derived(isStatic ? content : parseBlocks(content, streamdown.extensions));
131
158
  </script>
132
159
 
133
160
  <div bind:this={element} class={className}>
134
- {#each blocks as block, index (`${id}-block-${index}`)}
135
- <Block {block} />
136
- {/each}
161
+ {#if isStatic}
162
+ <Block static={isStatic} block={content} />
163
+ {:else}
164
+ {#each blocks as block, index (`${id}-block-${index}`)}
165
+ <Block static={isStatic} {block} />
166
+ {/each}
167
+ {/if}
137
168
  </div>
138
169
 
139
170
  <style global>
@@ -2,10 +2,11 @@ import type { Component, Snippet } from 'svelte';
2
2
  import type { DeepPartialTheme, Theme } from './theme.js';
3
3
  import type { MermaidConfig } from 'mermaid';
4
4
  import type { KatexOptions } from 'katex';
5
- import type { BundledTheme } from 'shiki';
5
+ import type { LanguageInfo } from './utils/bundledLanguages.js';
6
+ import type { ThemeRegistration } from 'shiki';
6
7
  export interface StreamdownContext extends Omit<StreamdownProps, keyof Snippets | 'class' | 'theme' | 'shikiTheme' | 'inlineCitationsMode'> {
7
8
  snippets: Snippets;
8
- shikiTheme: BundledTheme;
9
+ shikiTheme: string;
9
10
  theme: Theme;
10
11
  controls: {
11
12
  code: boolean;
@@ -91,6 +92,7 @@ export type Snippets<Source extends Record<string, any> = Record<string, any>> =
91
92
  };
92
93
  export type StreamdownProps<Source extends Record<string, any> = Record<string, any>> = {
93
94
  streamdown?: StreamdownContext;
95
+ static?: boolean;
94
96
  sources?: {
95
97
  [key: string]: Source;
96
98
  };
@@ -105,8 +107,9 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
105
107
  theme?: DeepPartialTheme;
106
108
  baseTheme?: 'tailwind' | 'shadcn';
107
109
  mergeTheme?: boolean;
108
- shikiTheme?: BundledTheme;
109
- shikiPreloadThemes?: BundledTheme[];
110
+ shikiTheme?: string;
111
+ shikiLanguages?: LanguageInfo[];
112
+ shikiThemes?: Record<string, ThemeRegistration>;
110
113
  mermaidConfig?: MermaidConfig;
111
114
  katexConfig?: KatexOptions | ((inline: boolean) => KatexOptions);
112
115
  translations?: {
@@ -159,5 +162,19 @@ export type StreamdownProps<Source extends Record<string, any> = Record<string,
159
162
  children: Snippet;
160
163
  props: any;
161
164
  }, any, any>>;
165
+ components?: {
166
+ code?: Component<{
167
+ token: Tokens.Code;
168
+ id: string;
169
+ }, any, any>;
170
+ mermaid?: Component<{
171
+ token: Tokens.Code;
172
+ id: string;
173
+ }, any, any>;
174
+ math?: Component<{
175
+ token: MathToken;
176
+ id: string;
177
+ }, any, any>;
178
+ };
162
179
  } & Partial<Snippets<Source>>;
163
180
  export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as Streamdown } from './Streamdown.svelte';
2
2
  export { useStreamdown, type StreamdownProps } from './context.svelte.js';
3
3
  export { theme, shadcnTheme, mergeTheme, type Theme } from './theme.js';
4
- export { type Extension, type StreamdownToken } from './marked/index.js';
5
- export { lex, parseBlocks } from './marked/index.js';
4
+ export { type Extension, type StreamdownToken, lex, parseBlocks } from './marked/index.js';
5
+ export { parseIncompleteMarkdown, type Plugin, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
6
+ export { bundledLanguagesInfo, createLanguageSet, type LanguageInfo } from './utils/bundledLanguages.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as Streamdown } from './Streamdown.svelte';
2
2
  export { useStreamdown } from './context.svelte.js';
3
3
  export { theme, shadcnTheme, mergeTheme } from './theme.js';
4
- export {} from './marked/index.js';
5
4
  export { lex, parseBlocks } from './marked/index.js';
5
+ export { parseIncompleteMarkdown, IncompleteMarkdownParser } from './utils/parse-incomplete-markdown.js';
6
+ export { bundledLanguagesInfo, createLanguageSet } from './utils/bundledLanguages.js';
@@ -1,6 +1,9 @@
1
1
  import {} from './index.js';
2
2
  import { StreamdownContext } from '../context.svelte.js';
3
3
  import { getContext } from 'svelte';
4
+ const footnoteRegex = /^\[\^([^\]\n]+)\]:(?:[ \t]+|\n|$)([^\n]*(?:\n(?:[ \t]+[^\n]*)?)*)/;
5
+ const footnoteRefRegex = /^\[\^([^\]\n]+)\]/;
6
+ const footNoteLastLineRegex = /^[ \t]*?[>\-*][ ]|[`]{3,}$|^[ \t]*?[|].+[|]$/;
4
7
  const safeGetContext = () => {
5
8
  try {
6
9
  return getContext('streamdown');
@@ -32,18 +35,16 @@ export function markedFootnote() {
32
35
  level: 'block',
33
36
  tokenizer(src) {
34
37
  const maps = ensureMaps(this);
35
- const match = /^\[\^([^\]\n]+)\]:(?:[ \t]+|[\n]*?|$)([^\n]*?(?:\n|$)(?:\n*?[ ]{4,}[^\n]*)*)/.exec(src);
38
+ const match = footnoteRegex.exec(src);
36
39
  if (match) {
37
40
  const [raw, label, text = ''] = match;
38
41
  let content = text.split('\n').reduce((acc, curr) => {
39
- return acc + '\n' + curr.replace(/^(?:[ ]{4}|[\t])/, '');
42
+ return acc + '\n' + curr.replace(/^[ \t]+/, '');
40
43
  }, '');
41
44
  const contentLastLine = content.trimEnd().split('\n').pop();
42
45
  content +=
43
46
  // add lines after list, blockquote, codefence, and table
44
- contentLastLine && /^[ \t]*?[>\-*][ ]|[`]{3,}$|^[ \t]*?[|].+[|]$/.test(contentLastLine)
45
- ? '\n\n'
46
- : '';
47
+ contentLastLine && footNoteLastLineRegex.test(contentLastLine) ? '\n\n' : '';
47
48
  const lines = content.split('\n');
48
49
  const token = {
49
50
  type: 'footnote',
@@ -66,7 +67,7 @@ export function markedFootnote() {
66
67
  level: 'inline',
67
68
  tokenizer(src) {
68
69
  const maps = ensureMaps(this);
69
- const match = /^\[\^([^\]\n]+)\]/.exec(src);
70
+ const match = footnoteRefRegex.exec(src);
70
71
  if (match) {
71
72
  const [raw, label] = match;
72
73
  const footnote = maps.footnotes.get(label);
@@ -0,0 +1,8 @@
1
+ export type LanguageInfo = {
2
+ id: string;
3
+ aliases?: string[];
4
+ import: () => Promise<any>;
5
+ };
6
+ export declare const bundledLanguagesInfo: LanguageInfo[];
7
+ export declare function createLanguageSet(languages: LanguageInfo[]): Set<string>;
8
+ export declare const supportedLanguages: Set<string>;
@@ -0,0 +1,143 @@
1
+ export const bundledLanguagesInfo = [
2
+ // Web essentials
3
+ {
4
+ id: 'javascript',
5
+ aliases: ['js'],
6
+ import: () => import('@shikijs/langs/javascript')
7
+ },
8
+ {
9
+ id: 'typescript',
10
+ aliases: ['ts'],
11
+ import: () => import('@shikijs/langs/typescript')
12
+ },
13
+ {
14
+ id: 'html',
15
+ import: () => import('@shikijs/langs/html')
16
+ },
17
+ {
18
+ id: 'css',
19
+ import: () => import('@shikijs/langs/css')
20
+ },
21
+ {
22
+ id: 'json',
23
+ import: () => import('@shikijs/langs/json')
24
+ },
25
+ {
26
+ id: 'jsx',
27
+ import: () => import('@shikijs/langs/jsx')
28
+ },
29
+ {
30
+ id: 'tsx',
31
+ import: () => import('@shikijs/langs/tsx')
32
+ },
33
+ {
34
+ id: 'markdown',
35
+ aliases: ['md'],
36
+ import: () => import('@shikijs/langs/markdown')
37
+ },
38
+ {
39
+ id: 'yaml',
40
+ aliases: ['yml'],
41
+ import: () => import('@shikijs/langs/yaml')
42
+ },
43
+ {
44
+ id: 'xml',
45
+ import: () => import('@shikijs/langs/xml')
46
+ },
47
+ // Backend languages
48
+ {
49
+ id: 'python',
50
+ aliases: ['py'],
51
+ import: () => import('@shikijs/langs/python')
52
+ },
53
+ {
54
+ id: 'java',
55
+ import: () => import('@shikijs/langs/java')
56
+ },
57
+ {
58
+ id: 'go',
59
+ import: () => import('@shikijs/langs/go')
60
+ },
61
+ {
62
+ id: 'rust',
63
+ aliases: ['rs'],
64
+ import: () => import('@shikijs/langs/rust')
65
+ },
66
+ {
67
+ id: 'ruby',
68
+ aliases: ['rb'],
69
+ import: () => import('@shikijs/langs/ruby')
70
+ },
71
+ {
72
+ id: 'php',
73
+ import: () => import('@shikijs/langs/php')
74
+ },
75
+ {
76
+ id: 'c',
77
+ import: () => import('@shikijs/langs/c')
78
+ },
79
+ {
80
+ id: 'cpp',
81
+ aliases: ['c++'],
82
+ import: () => import('@shikijs/langs/cpp')
83
+ },
84
+ {
85
+ id: 'csharp',
86
+ aliases: ['c#', 'cs'],
87
+ import: () => import('@shikijs/langs/csharp')
88
+ },
89
+ {
90
+ id: 'sql',
91
+ import: () => import('@shikijs/langs/sql')
92
+ },
93
+ {
94
+ id: 'swift',
95
+ import: () => import('@shikijs/langs/swift')
96
+ },
97
+ {
98
+ id: 'kotlin',
99
+ aliases: ['kt', 'kts'],
100
+ import: () => import('@shikijs/langs/kotlin')
101
+ },
102
+ // Config/Shell
103
+ {
104
+ id: 'shellscript',
105
+ aliases: ['bash', 'sh', 'shell', 'zsh'],
106
+ import: () => import('@shikijs/langs/shellscript')
107
+ },
108
+ {
109
+ id: 'docker',
110
+ aliases: ['dockerfile'],
111
+ import: () => import('@shikijs/langs/docker')
112
+ },
113
+ {
114
+ id: 'toml',
115
+ import: () => import('@shikijs/langs/toml')
116
+ },
117
+ {
118
+ id: 'graphql',
119
+ aliases: ['gql'],
120
+ import: () => import('@shikijs/langs/graphql')
121
+ },
122
+ {
123
+ id: 'svelte',
124
+ import: () => import('@shikijs/langs/svelte')
125
+ },
126
+ {
127
+ id: 'vue',
128
+ import: () => import('@shikijs/langs/vue')
129
+ }
130
+ ];
131
+ // Helper function to create a Set of all language IDs and aliases from language info
132
+ export function createLanguageSet(languages) {
133
+ const set = new Set();
134
+ languages.forEach((lang) => {
135
+ set.add(lang.id);
136
+ if (lang.aliases) {
137
+ lang.aliases.forEach((alias) => set.add(alias));
138
+ }
139
+ });
140
+ return set;
141
+ }
142
+ // Create a Set of all supported language IDs and aliases for fast lookup (default set)
143
+ export const supportedLanguages = createLanguageSet(bundledLanguagesInfo);
@@ -0,0 +1,3 @@
1
+ export declare const useDarkMode: () => {
2
+ readonly current: boolean;
3
+ };
@@ -0,0 +1,49 @@
1
+ import { onMount } from 'svelte';
2
+ export const useDarkMode = () => {
3
+ let isDark = $state(false);
4
+ const checkTheme = () => {
5
+ const elements = [document.documentElement, document.body];
6
+ // First check for explicit light mode
7
+ for (const element of elements) {
8
+ if (element.classList.contains('light') ||
9
+ element.dataset.theme === 'light' ||
10
+ element.style.colorScheme === 'light') {
11
+ isDark = false;
12
+ return;
13
+ }
14
+ }
15
+ // Then check for dark mode
16
+ for (const element of elements) {
17
+ if (element.classList.contains('dark') ||
18
+ element.dataset.theme === 'dark' ||
19
+ element.style.colorScheme === 'dark') {
20
+ isDark = true;
21
+ return;
22
+ }
23
+ }
24
+ // Default to light mode if no explicit theme is set
25
+ isDark = false;
26
+ };
27
+ onMount(() => {
28
+ // Initial check
29
+ checkTheme();
30
+ // Watch for class, attribute, and style changes on html and body
31
+ const observer = new MutationObserver(() => {
32
+ checkTheme();
33
+ });
34
+ const observerOptions = {
35
+ attributes: true,
36
+ attributeFilter: ['class', 'data-theme', 'style']
37
+ };
38
+ observer.observe(document.documentElement, observerOptions);
39
+ observer.observe(document.body, observerOptions);
40
+ return () => {
41
+ observer.disconnect();
42
+ };
43
+ });
44
+ return {
45
+ get current() {
46
+ return isDark;
47
+ }
48
+ };
49
+ };
@@ -1,32 +1,30 @@
1
- import { type BundledLanguage, type BundledTheme, type HighlighterGeneric, type ThemedToken } from 'shiki';
2
- import { SvelteMap, SvelteSet } from 'svelte/reactivity';
3
- export declare const loadShiki: () => Promise<[any, import("shiki").CreateHighlighterFactory<BundledLanguage, BundledTheme>]>;
4
- export type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>;
1
+ import type { ThemedToken, ThemeRegistration } from 'shiki';
2
+ import type { HighlighterCore } from 'shiki/core';
3
+ import { SvelteMap } from 'svelte/reactivity';
4
+ import { type LanguageInfo } from './bundledLanguages.js';
5
+ export type Highlighter = HighlighterCore;
5
6
  declare class HighlighterManager {
6
- loadedLanguages: SvelteMap<BundledLanguage, boolean | Promise<void>>;
7
- loadedThemes: SvelteMap<BundledTheme, boolean | Promise<void>>;
8
- preloadedThemes: SvelteSet<BundledTheme>;
9
- highlighter: Highlighter | Promise<Highlighter> | null;
10
- constructor(preloadedThemes: BundledTheme[]);
7
+ loadedLanguages: SvelteMap<string, boolean | Promise<void>>;
8
+ highlighter: any;
9
+ customLanguages: Set<string>;
10
+ languageLoaders: Map<string, () => Promise<any>>;
11
+ additionalThemes: Record<string, ThemeRegistration>;
12
+ constructor(languages: LanguageInfo[], additionalThemes?: Record<string, ThemeRegistration>, additionalLanguages?: LanguageInfo[]);
11
13
  private loadHighlighter;
12
- private loadTheme;
14
+ private isThemeAvailable;
13
15
  private loadLanguage;
14
16
  private isLanguageSupported;
15
- isReady(theme: BundledTheme, language?: string | undefined): boolean;
16
- /**
17
- * Preloads themes by creating minimal highlighter instances.
18
- * This reduces flickering when switching themes.
19
- */
20
- preloadThemes(highlighter: Highlighter): Promise<void>;
17
+ isReady(theme: string, language: string | undefined): boolean;
21
18
  /**
22
19
  * Ensures the highlighter is ready for the given theme and language.
23
20
  */
24
- load(theme: BundledTheme, language?: string | undefined): Promise<void>;
21
+ load(theme: string, language: string | undefined): Promise<void>;
25
22
  /**
26
23
  * Highlights code synchronously. Must call isReady() first.
24
+ * Returns plaintext tokens for unsupported languages.
27
25
  */
28
- highlightCode(code: string, language: string | undefined, theme: BundledTheme): ThemedToken[][];
29
- static create(preloadedThemes?: BundledTheme[]): HighlighterManager;
26
+ highlightCode(code: string, language: string | undefined, theme: string): ThemedToken[][];
27
+ static create(languages?: LanguageInfo[], additionalThemes?: Record<string, ThemeRegistration>, additionalLanguages?: LanguageInfo[]): HighlighterManager;
30
28
  }
31
29
  export { HighlighterManager };
32
30
  export declare const languageExtensionMap: Record<string, string>;