svelte-streamdown 1.0.1 → 1.0.3

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.
@@ -5,12 +5,13 @@
5
5
  import { useStreamdown } from '../Streamdown.svelte';
6
6
  import { save } from '../utils/save.js';
7
7
  import { useCopy } from '../utils/copy.svelte.js';
8
- import { highlighter, languageExtensionMap } from '../hightlighter.svelte.js';
8
+ import { HighlighterManager, languageExtensionMap } from '../hightlighter.svelte.js';
9
9
  import { clsx } from 'clsx';
10
10
  import type { ElementProps } from './element.js';
11
11
 
12
12
  let { node, className, props }: ElementProps = $props();
13
13
 
14
+ const highlighter = HighlighterManager.create();
14
15
  const streamdown = useStreamdown();
15
16
  const theme = $derived(streamdown.shikiTheme);
16
17
  let codeContent = $derived((node.children[0] as any).value);
@@ -38,8 +39,10 @@
38
39
  };
39
40
 
40
41
  $effect(() => {
41
- void highlighter.isReady(theme, language as any);
42
+ void highlighter.load(theme, language as any);
42
43
  });
44
+
45
+ $inspect(highlighter.isReady(theme, language as any));
43
46
  </script>
44
47
 
45
48
  <div
@@ -67,7 +70,7 @@
67
70
  </div>
68
71
  <div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
69
72
  <div>
70
- {#if highlighter.isLoaded(theme, language as any)}
73
+ {#if highlighter.isReady(theme, language as any)}
71
74
  {@const code = highlighter.highlightCode(
72
75
  codeContent,
73
76
  language as any,
@@ -30,7 +30,6 @@
30
30
  import 'katex/dist/katex.min.css';
31
31
  import { mergeTheme, type Theme, shadcnTheme } from './theme.js';
32
32
  import type { BundledTheme } from 'shiki';
33
- import { highlighter } from './hightlighter.svelte.js';
34
33
 
35
34
  let {
36
35
  content = '',
@@ -133,13 +132,6 @@
133
132
  const id = $props.id();
134
133
 
135
134
  const blocks = $derived(parseMarkdownIntoBlocks(content));
136
-
137
- // Preload themes if specified to avoid flickering
138
- $effect(() => {
139
- if (shikiPreloadThemes && shikiPreloadThemes.length > 0) {
140
- highlighter.preloadThemes(shikiPreloadThemes);
141
- }
142
- });
143
135
  </script>
144
136
 
145
137
  {#each blocks as block, index (`${id}-block-${index}`)}
@@ -1,12 +1,16 @@
1
1
  import { type BundledLanguage, type BundledTheme } from 'shiki';
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+ export declare const loadShiki: () => Promise<[any, import("shiki").CreateHighlighterFactory<BundledLanguage, BundledTheme>]>;
2
4
  declare class HighlighterManager {
3
5
  initialized: boolean;
4
- private state;
5
- private readonly preloadedThemes;
6
- private readonly readyCache;
7
- initPromise: Promise<void> | null;
8
- private getCacheKey;
9
- isLoaded(theme: BundledTheme, language: BundledLanguage): boolean;
6
+ private highlighter;
7
+ private highlighters;
8
+ private createHighlighter;
9
+ private engine;
10
+ loadedLanguages: SvelteSet<BundledLanguage>;
11
+ loadedThemes: SvelteSet<BundledTheme>;
12
+ constructor();
13
+ isReady(theme: BundledTheme, language: BundledLanguage): boolean;
10
14
  /**
11
15
  * Preloads themes by creating minimal highlighter instances.
12
16
  * This reduces flickering when switching themes.
@@ -15,17 +19,12 @@ declare class HighlighterManager {
15
19
  /**
16
20
  * Ensures the highlighter is ready for the given theme and language.
17
21
  */
18
- isReady(theme: BundledTheme, language: BundledLanguage): Promise<void>;
19
- private initialize;
22
+ load(theme: BundledTheme, language: BundledLanguage): Promise<void>;
20
23
  /**
21
24
  * Highlights code synchronously. Must call isReady() first.
22
25
  */
23
26
  highlightCode(code: string, language: BundledLanguage, theme: BundledTheme, preClassName?: string): string;
24
- /**
25
- * Clean up resources
26
- */
27
- dispose(): void;
27
+ static create(): HighlighterManager;
28
28
  }
29
29
  export { HighlighterManager };
30
- export declare const highlighter: HighlighterManager;
31
30
  export declare const languageExtensionMap: Record<string, string>;
@@ -1,39 +1,48 @@
1
- import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
2
- import { createHighlighter } from 'shiki';
3
- import { SvelteMap } from 'svelte/reactivity';
1
+ import {} from 'shiki';
2
+ import { untrack } from 'svelte';
3
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
4
4
  // Remove background styles from <pre> tags (inline style)
5
5
  const removePreBackground = (html) => {
6
6
  return html.replace(/<pre[^>]*style="[^"]*background[^";]*;?[^"]*"[^>]*>/g, (match) => match.replace(/style="[^"]*background[^";]*;?[^"]*"/, ''));
7
7
  };
8
+ export const loadShiki = async () => {
9
+ return Promise.all([
10
+ import('shiki/engine/javascript').then((mod) => mod.createJavaScriptRegexEngine({ forgiving: true })),
11
+ import('shiki').then((mod) => mod.createHighlighter)
12
+ ]);
13
+ };
8
14
  class HighlighterManager {
9
15
  initialized = $state(false);
10
- state = null;
11
- preloadedThemes = new Set();
12
- readyCache = new SvelteMap();
13
- initPromise = $state(null);
14
- getCacheKey(theme, language) {
15
- return `${theme}:${language}`;
16
+ highlighter = null;
17
+ highlighters = new SvelteMap();
18
+ createHighlighter = null;
19
+ engine = null;
20
+ loadedLanguages = new SvelteSet();
21
+ loadedThemes = new SvelteSet();
22
+ constructor() {
23
+ if (typeof window !== 'undefined') {
24
+ Object.assign(window, {
25
+ STREAMDOWN_HIGHLIGHTER: this
26
+ });
27
+ }
16
28
  }
17
- isLoaded(theme, language) {
18
- return this.readyCache.get(this.getCacheKey(theme, language)) ?? false;
29
+ isReady(theme, language) {
30
+ return this.highlighters.has(`${theme}:${language}`);
19
31
  }
20
32
  /**
21
33
  * Preloads themes by creating minimal highlighter instances.
22
34
  * This reduces flickering when switching themes.
23
35
  */
24
36
  async preloadThemes(themes) {
37
+ if (!this.highlighter) {
38
+ return;
39
+ }
25
40
  const promises = themes
26
- .filter((theme) => !this.preloadedThemes.has(theme))
41
+ .filter((theme) => !this.loadedThemes.has(theme))
27
42
  .map(async (theme) => {
28
43
  try {
29
- // Create minimal highlighter just to load theme assets
30
- const preloader = await createHighlighter({
31
- themes: [theme],
32
- langs: ['javascript'], // Minimal language for preloading
33
- engine: createJavaScriptRegexEngine({ forgiving: true })
34
- });
35
- preloader.dispose();
36
- this.preloadedThemes.add(theme);
44
+ this.highlighter.loadTheme(theme);
45
+ this.loadedThemes.add(theme);
37
46
  }
38
47
  catch (error) {
39
48
  console.warn(`Failed to preload theme ${theme}:`, error);
@@ -44,66 +53,59 @@ class HighlighterManager {
44
53
  /**
45
54
  * Ensures the highlighter is ready for the given theme and language.
46
55
  */
47
- async isReady(theme, language) {
48
- const cacheKey = this.getCacheKey(theme, language);
49
- // Return immediately if already ready
50
- if (this.readyCache.get(cacheKey)) {
51
- return;
52
- }
53
- // Wait for any ongoing initialization
54
- if (this.initPromise) {
55
- await this.initPromise;
56
- }
57
- // Check again after waiting
58
- if (this.readyCache.get(cacheKey)) {
59
- return;
60
- }
61
- // Initialize the highlighter
62
- this.initPromise = this.initialize(theme, language);
63
- await this.initPromise;
64
- this.readyCache.set(cacheKey, true);
65
- this.initialized = true;
66
- this.initPromise = null;
67
- }
68
- async initialize(theme, language) {
69
- const needsNewHighlighter = !this.state || this.state.theme !== theme;
70
- if (needsNewHighlighter) {
71
- // Dispose old highlighter if it exists
72
- this.state?.highlighter.dispose();
73
- // Clear cache when theme changes
74
- this.readyCache.clear();
75
- // Create new highlighter with the theme
76
- const highlighter = await createHighlighter({
77
- themes: [theme],
78
- langs: [language],
79
- engine: createJavaScriptRegexEngine({ forgiving: true })
80
- });
81
- this.state = {
82
- highlighter,
83
- theme,
84
- languages: new Set([language])
85
- };
86
- // Mark theme as preloaded since we now have it
87
- this.preloadedThemes.add(theme);
88
- }
89
- else if (this.state && !this.state.languages.has(language)) {
90
- // Add language to existing highlighter
91
- await this.state.highlighter.loadLanguage(language);
92
- this.state.languages.add(language);
93
- }
56
+ async load(theme, language) {
57
+ return untrack(async () => {
58
+ console.log('load', theme, language);
59
+ if (!this.createHighlighter || !this.engine) {
60
+ const [engine, createHighlighter] = await loadShiki();
61
+ this.createHighlighter = createHighlighter;
62
+ this.engine = engine;
63
+ }
64
+ if (!this.highlighters.has(`${theme}:${language}`)) {
65
+ const highlighter = await this.createHighlighter({
66
+ themes: [theme],
67
+ langs: [language],
68
+ engine: this.engine
69
+ });
70
+ this.highlighters.set(`${theme}:${language}`, highlighter);
71
+ }
72
+ // if (!highlighter) {
73
+ // highlighter = await this.createHighlighter({
74
+ // themes: [theme],
75
+ // langs: [language],
76
+ // engine: this.engine
77
+ // });
78
+ // this.highlighter = highlighter;
79
+ // this.loadedThemes.add(theme);
80
+ // this.loadedLanguages.add(language);
81
+ // }
82
+ // if (!this.loadedThemes.has(theme)) {
83
+ // console.log('loading theme', theme);
84
+ // await this.highlighter.loadTheme(theme);
85
+ // }
86
+ // if (!this.loadedLanguages.has(language)) {
87
+ // console.log('loading language', language);
88
+ // await this.highlighter.loadLanguage(language);
89
+ // }
90
+ // this.loadedThemes.add(theme);
91
+ // this.loadedLanguages.add(language);
92
+ });
94
93
  }
95
94
  /**
96
95
  * Highlights code synchronously. Must call isReady() first.
97
96
  */
98
97
  highlightCode(code, language, theme, preClassName) {
99
- if (!this.isLoaded(theme, language)) {
98
+ console.log('highlightCode', theme, language, this.isReady(theme, language));
99
+ if (!this.isReady(theme, language)) {
100
100
  throw new Error(`Highlighter not ready for theme "${theme}" and language "${language}". ` +
101
101
  `Call await highlighter.isReady(theme, language) first.`);
102
102
  }
103
- if (!this.state) {
104
- throw new Error('Highlighter not initialized. This should not happen after calling isReady().');
103
+ const highlighter = this.highlighters.get(`${theme}:${language}`);
104
+ if (!highlighter) {
105
+ throw new Error(`Highlighter not loaded for theme "${theme}" and language "${language}". ` +
106
+ `Call await highlighter.load(theme, language) first.`);
105
107
  }
106
- let html = this.state.highlighter.codeToHtml(code, {
108
+ let html = highlighter.codeToHtml(code, {
107
109
  lang: language,
108
110
  theme: theme
109
111
  });
@@ -114,21 +116,16 @@ class HighlighterManager {
114
116
  }
115
117
  return html;
116
118
  }
117
- /**
118
- * Clean up resources
119
- */
120
- dispose() {
121
- this.state?.highlighter.dispose();
122
- this.state = null;
123
- this.readyCache.clear();
124
- this.preloadedThemes.clear();
125
- this.initialized = false;
119
+ static create() {
120
+ if (typeof window !== 'undefined' && 'STREAMDOWN_HIGHLIGHTER' in window) {
121
+ // @ts-ignore
122
+ return window.STREAMDOWN_HIGHLIGHTER;
123
+ }
124
+ return new HighlighterManager();
126
125
  }
127
126
  }
128
127
  // Export the class for those who want to create their own instances
129
128
  export { HighlighterManager };
130
- // Create and export a singleton instance
131
- export const highlighter = new HighlighterManager();
132
129
  export const languageExtensionMap = {
133
130
  '1c': '1c',
134
131
  '1c-query': '1cq',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",