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.
@@ -1,18 +1,41 @@
1
- import { bundledLanguages } from 'shiki';
2
- import { SvelteMap, SvelteSet } from 'svelte/reactivity';
3
- export const loadShiki = async () => {
4
- return Promise.all([
5
- import('shiki/engine/javascript').then((mod) => mod.createJavaScriptRegexEngine({ forgiving: true })),
6
- import('shiki').then((mod) => mod.createHighlighter)
7
- ]);
1
+ import { SvelteMap } from 'svelte/reactivity';
2
+ import { supportedLanguages, createLanguageSet, bundledLanguagesInfo } from './bundledLanguages.js';
3
+ import { createHighlighterCore } from 'shiki/core';
4
+ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
5
+ // Import default themes directly
6
+ import githubDark from '@shikijs/themes/github-dark';
7
+ import githubLight from '@shikijs/themes/github-light';
8
+ // Default themes that are always loaded
9
+ const DEFAULT_THEMES = {
10
+ 'github-dark': githubDark,
11
+ 'github-light': githubLight
8
12
  };
9
13
  class HighlighterManager {
10
14
  loadedLanguages = new SvelteMap();
11
- loadedThemes = new SvelteMap();
12
- preloadedThemes = new SvelteSet();
13
15
  highlighter = $state(null);
14
- constructor(preloadedThemes) {
15
- preloadedThemes.forEach((theme) => this.preloadedThemes.add(theme));
16
+ customLanguages;
17
+ languageLoaders;
18
+ additionalThemes;
19
+ constructor(languages, additionalThemes, additionalLanguages) {
20
+ // Store additional themes (will be loaded with highlighter)
21
+ this.additionalThemes = additionalThemes || {};
22
+ // Merge languages: default + additional
23
+ const allLanguages = additionalLanguages ? [...languages, ...additionalLanguages] : languages;
24
+ this.languageLoaders = new Map();
25
+ allLanguages.forEach((l) => {
26
+ this.languageLoaders.set(l.id, l.import);
27
+ if (l.aliases) {
28
+ l.aliases.forEach((alias) => this.languageLoaders.set(alias, l.import));
29
+ }
30
+ });
31
+ // Build custom languages set for validation
32
+ if (additionalLanguages) {
33
+ const additionalSet = createLanguageSet(additionalLanguages);
34
+ this.customLanguages = new Set([...supportedLanguages, ...additionalSet]);
35
+ }
36
+ else {
37
+ this.customLanguages = supportedLanguages;
38
+ }
16
39
  if (typeof window !== 'undefined') {
17
40
  Object.assign(window, {
18
41
  STREAMDOWN_HIGHLIGHTER: this
@@ -24,17 +47,25 @@ class HighlighterManager {
24
47
  return this.highlighter;
25
48
  }
26
49
  else if (!this.highlighter) {
27
- this.highlighter = new Promise((resolve, reject) => {
28
- loadShiki().then(([engine, createHighlighter]) => {
29
- return createHighlighter({
30
- themes: [],
50
+ this.highlighter = new Promise(async (resolve, reject) => {
51
+ try {
52
+ const engine = createJavaScriptRegexEngine({ forgiving: true });
53
+ // Load default themes + any additional themes immediately
54
+ const allThemes = [
55
+ ...Object.values(DEFAULT_THEMES),
56
+ ...Object.values(this.additionalThemes)
57
+ ];
58
+ const highlighter = await createHighlighterCore({
59
+ themes: allThemes,
31
60
  langs: [],
32
61
  engine
33
- }).then((highlighter) => {
34
- this.highlighter = highlighter;
35
- resolve(highlighter);
36
62
  });
37
- });
63
+ this.highlighter = highlighter;
64
+ resolve(highlighter);
65
+ }
66
+ catch (error) {
67
+ reject(error);
68
+ }
38
69
  });
39
70
  return this.highlighter;
40
71
  }
@@ -42,94 +73,143 @@ class HighlighterManager {
42
73
  return this.highlighter;
43
74
  }
44
75
  }
45
- async loadTheme(theme, highlighter) {
46
- const themeLoader = this.loadedThemes.get(theme);
47
- if (themeLoader instanceof Promise) {
48
- await themeLoader;
49
- }
50
- else {
51
- const themeLoaderPromise = highlighter.loadTheme(theme).then(() => {
52
- this.loadedThemes.set(theme, true);
53
- });
54
- this.loadedThemes.set(theme, themeLoaderPromise);
55
- await themeLoaderPromise;
56
- }
76
+ isThemeAvailable(theme) {
77
+ return theme in DEFAULT_THEMES || theme in this.additionalThemes;
57
78
  }
58
79
  async loadLanguage(language, highlighter) {
80
+ // Skip loading if language is not supported
81
+ if (!this.isLanguageSupported(language)) {
82
+ return;
83
+ }
59
84
  const languageLoader = this.loadedLanguages.get(language);
60
85
  if (languageLoader instanceof Promise) {
61
86
  await languageLoader;
62
87
  }
63
- else {
64
- const languageLoaderPromise = highlighter.loadLanguage(language).then(() => {
88
+ else if (!languageLoader) {
89
+ const loader = this.languageLoaders.get(language);
90
+ if (!loader) {
91
+ // Language not available, mark as failed
92
+ this.loadedLanguages.set(language, false);
93
+ return;
94
+ }
95
+ const languageLoaderPromise = loader()
96
+ .then((langModule) => {
97
+ const langObj = langModule.default || langModule;
98
+ return highlighter.loadLanguage(langObj);
99
+ })
100
+ .then(() => {
65
101
  this.loadedLanguages.set(language, true);
102
+ })
103
+ .catch((err) => {
104
+ this.loadedLanguages.set(language, false);
105
+ throw err;
66
106
  });
67
107
  this.loadedLanguages.set(language, languageLoaderPromise);
68
108
  await languageLoaderPromise;
69
109
  }
70
110
  }
71
111
  isLanguageSupported = (language) => {
72
- return Object.hasOwn(bundledLanguages, language);
112
+ return this.customLanguages.has(language);
73
113
  };
74
- isReady(theme, language = 'bash') {
114
+ isReady(theme, language) {
115
+ // Check if theme is available
116
+ if (!this.isThemeAvailable(theme)) {
117
+ return false;
118
+ }
119
+ // For unsupported languages, we don't need to load anything (will use plaintext)
120
+ if (!language || !this.isLanguageSupported(language)) {
121
+ return !!this.highlighter && !(this.highlighter instanceof Promise);
122
+ }
75
123
  return (!!this.highlighter &&
76
124
  !(this.highlighter instanceof Promise) &&
77
- this.loadedThemes.get(theme) === true &&
78
- this.loadedLanguages.get(this.isLanguageSupported(language) ? language : 'bash') === true);
79
- }
80
- /**
81
- * Preloads themes by creating minimal highlighter instances.
82
- * This reduces flickering when switching themes.
83
- */
84
- async preloadThemes(highlighter) {
85
- await Promise.all(Array.from(this.preloadedThemes).map((theme) => this.loadTheme(theme, highlighter)));
125
+ this.loadedLanguages.get(language) === true);
86
126
  }
87
127
  /**
88
128
  * Ensures the highlighter is ready for the given theme and language.
89
129
  */
90
- async load(theme, language = 'bash') {
91
- const themeLoader = this.loadedThemes.get(theme);
92
- const languageLoader = this.loadedLanguages.get(this.isLanguageSupported(language) ? language : 'bash');
130
+ async load(theme, language) {
131
+ // For unsupported languages, only need to load highlighter (themes are pre-loaded)
132
+ if (!language || !this.isLanguageSupported(language)) {
133
+ if (this.highlighter !== null && !(this.highlighter instanceof Promise)) {
134
+ return;
135
+ }
136
+ await this.loadHighlighter();
137
+ return;
138
+ }
139
+ const languageLoader = this.loadedLanguages.get(language);
93
140
  if (this.highlighter !== null &&
94
141
  !(this.highlighter instanceof Promise) &&
95
- themeLoader === true &&
96
142
  languageLoader === true) {
97
143
  return;
98
144
  }
99
145
  const highlighter = await this.loadHighlighter();
100
- await Promise.all([
101
- this.loadTheme(theme, highlighter),
102
- this.loadLanguage(this.isLanguageSupported(language) ? language : 'bash', highlighter)
103
- ]);
104
- await this.preloadThemes(highlighter);
146
+ await this.loadLanguage(language, highlighter);
105
147
  }
106
148
  /**
107
149
  * Highlights code synchronously. Must call isReady() first.
150
+ * Returns plaintext tokens for unsupported languages.
108
151
  */
109
- highlightCode(code, language = 'bash', theme) {
152
+ highlightCode(code, language, theme) {
110
153
  try {
111
- // const highlighter = this.highlighters.get(`${theme}:${language}`);
112
154
  const highlighter = this.highlighter;
113
155
  if (!highlighter || highlighter instanceof Promise) {
114
- return [];
156
+ // Return plaintext tokens when highlighter is not ready
157
+ return code.split('\n').map((line) => [
158
+ {
159
+ content: line,
160
+ color: undefined,
161
+ bgColor: undefined
162
+ }
163
+ ]);
164
+ }
165
+ // For unsupported languages, return plaintext tokens
166
+ if (!language || !this.isLanguageSupported(language)) {
167
+ return code.split('\n').map((line) => [
168
+ {
169
+ content: line,
170
+ color: undefined,
171
+ bgColor: undefined
172
+ }
173
+ ]);
115
174
  }
116
175
  const tokens = highlighter.codeToTokensBase(code, {
117
- lang: this.isLanguageSupported(language) ? language : 'bash',
176
+ lang: language,
118
177
  theme
119
178
  });
120
179
  return tokens;
121
180
  }
122
181
  catch (error) {
123
- return [];
182
+ // Return plaintext tokens on error
183
+ return code.split('\n').map((line) => [
184
+ {
185
+ content: line,
186
+ color: undefined,
187
+ bgColor: undefined
188
+ }
189
+ ]);
124
190
  }
125
191
  }
126
- static create(preloadedThemes = []) {
192
+ static create(languages = bundledLanguagesInfo, additionalThemes, additionalLanguages) {
127
193
  if (typeof window !== 'undefined' && 'STREAMDOWN_HIGHLIGHTER' in window) {
128
194
  const previousHighlighter = window.STREAMDOWN_HIGHLIGHTER;
129
- preloadedThemes.forEach((theme) => previousHighlighter.preloadedThemes.add(theme));
195
+ // Merge additional themes
196
+ if (additionalThemes) {
197
+ Object.assign(previousHighlighter.additionalThemes, additionalThemes);
198
+ }
199
+ // Merge additional languages with existing set
200
+ if (additionalLanguages) {
201
+ additionalLanguages.forEach((lang) => {
202
+ previousHighlighter.languageLoaders.set(lang.id, lang.import);
203
+ if (lang.aliases) {
204
+ lang.aliases.forEach((alias) => previousHighlighter.languageLoaders.set(alias, lang.import));
205
+ }
206
+ });
207
+ const additionalSet = createLanguageSet(additionalLanguages);
208
+ additionalSet.forEach((lang) => previousHighlighter.customLanguages.add(lang));
209
+ }
130
210
  return previousHighlighter;
131
211
  }
132
- return new HighlighterManager(preloadedThemes);
212
+ return new HighlighterManager(languages, additionalThemes, additionalLanguages);
133
213
  }
134
214
  }
135
215
  // Export the class for those who want to create their own instances
@@ -1 +1,53 @@
1
+ export interface Plugin {
2
+ name: string;
3
+ pattern?: RegExp;
4
+ handler?: (payload: HandlerPayload) => string;
5
+ skipInBlockTypes?: string[];
6
+ preprocess?: (payload: HookPayload) => string | {
7
+ text: string;
8
+ state: Partial<ParseState>;
9
+ };
10
+ postprocess?: (payload: HookPayload) => string;
11
+ }
12
+ interface HookPayload {
13
+ text: string;
14
+ state: ParseState;
15
+ setState: (state: Partial<ParseState>) => void;
16
+ }
17
+ interface HandlerPayload {
18
+ line: string;
19
+ text: string;
20
+ match: RegExpMatchArray;
21
+ state: ParseState;
22
+ setState: (state: Partial<ParseState>) => void;
23
+ }
24
+ interface ParseState {
25
+ currentLine: number;
26
+ context: 'normal' | 'list' | 'blockquote' | 'descriptionList';
27
+ blockingContexts: Set<'code' | 'math' | 'center' | 'right'>;
28
+ lineContexts?: Array<{
29
+ code: boolean;
30
+ math: boolean;
31
+ center: boolean;
32
+ right: boolean;
33
+ }>;
34
+ fenceInfo?: string;
35
+ mdxUnclosedTags?: Array<{
36
+ tagName: string;
37
+ lineIndex: number;
38
+ }>;
39
+ mdxLineStates?: Array<{
40
+ inMdx: boolean;
41
+ incompletePositions: number[];
42
+ }>;
43
+ }
44
+ export declare class IncompleteMarkdownParser {
45
+ private plugins;
46
+ private state;
47
+ setState: (state: Partial<ParseState>) => void;
48
+ constructor(plugins?: Plugin[]);
49
+ parse(text: string): string;
50
+ static createDefaultPlugins(): Plugin[];
51
+ }
1
52
  export declare const parseIncompleteMarkdown: (text: string) => string;
53
+ export {};
@@ -1,4 +1,4 @@
1
- class IncompleteMarkdownParser {
1
+ export class IncompleteMarkdownParser {
2
2
  plugins = [];
3
3
  state = {
4
4
  currentLine: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "2.6.1",
3
+ "version": "3.0.1",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -30,6 +30,18 @@
30
30
  ".": {
31
31
  "types": "./dist/index.d.ts",
32
32
  "svelte": "./dist/index.js"
33
+ },
34
+ "./code": {
35
+ "types": "./dist/Elements/Code.svelte.d.ts",
36
+ "svelte": "./dist/Elements/Code.svelte"
37
+ },
38
+ "./mermaid": {
39
+ "types": "./dist/Elements/Mermaid.svelte.d.ts",
40
+ "svelte": "./dist/Elements/Mermaid.svelte"
41
+ },
42
+ "./math": {
43
+ "types": "./dist/Elements/Math.svelte.d.ts",
44
+ "svelte": "./dist/Elements/Math.svelte"
33
45
  }
34
46
  },
35
47
  "peerDependencies": {
@@ -72,6 +84,8 @@
72
84
  },
73
85
  "dependencies": {
74
86
  "@floating-ui/dom": "^1.7.4",
87
+ "@shikijs/langs": "^3.17.1",
88
+ "@shikijs/themes": "^3.17.1",
75
89
  "clsx": "^2.1.1",
76
90
  "katex": "^0.16.22",
77
91
  "marked": "^16.2.1",