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 {
|
|
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.
|
|
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.
|
|
73
|
+
{#if highlighter.isReady(theme, language as any)}
|
|
71
74
|
{@const code = highlighter.highlightCode(
|
|
72
75
|
codeContent,
|
|
73
76
|
language as any,
|
package/dist/Streamdown.svelte
CHANGED
|
@@ -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
|
|
5
|
-
private
|
|
6
|
-
private
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
return this.
|
|
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.
|
|
41
|
+
.filter((theme) => !this.loadedThemes.has(theme))
|
|
27
42
|
.map(async (theme) => {
|
|
28
43
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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 =
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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',
|