svelte-streamdown 3.1.2 → 4.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.
- package/README.md +130 -43
- package/dist/Block.svelte +5 -2
- package/dist/Elements/Citation.svelte +2 -2
- package/dist/Elements/Code.svelte +21 -54
- package/dist/Elements/Code.svelte.d.ts +2 -2
- package/dist/Elements/Element.svelte +8 -5
- package/dist/Elements/Link.svelte +2 -1
- package/dist/Elements/Mermaid.svelte +8 -3
- package/dist/Elements/Mermaid.svelte.d.ts +2 -2
- package/dist/Elements/MermaidDownload.svelte +1 -1
- package/dist/Elements/TableDownload.svelte +13 -5
- package/dist/Elements/fallbacks/CodeFallback.svelte +7 -1
- package/dist/Elements/fallbacks/MermaidFallback.svelte +7 -1
- package/dist/Streamdown.svelte +12 -15
- package/dist/context.svelte.d.ts +9 -8
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/marked/index.d.ts +20 -1
- package/dist/marked/index.js +160 -39
- package/dist/marked/marked-br.js +6 -0
- package/dist/marked/marked-citations.js +6 -0
- package/dist/marked/marked-footnotes.js +18 -4
- package/dist/marked/marked-math.js +5 -0
- package/dist/marked/marked-subsup.js +24 -3
- package/dist/marked/marked-table.js +15 -12
- package/dist/theme.d.ts +0 -3
- package/dist/theme.js +0 -2
- package/dist/utils/highlightThemes.d.ts +4 -0
- package/dist/utils/highlightThemes.js +14 -0
- package/dist/utils/hightlighter.svelte.d.ts +6 -29
- package/dist/utils/hightlighter.svelte.js +27 -212
- package/dist/utils/parse-incomplete-markdown.js +100 -18
- package/dist/utils/save.js +4 -1
- package/dist/utils/url.js +6 -2
- package/package.json +5 -6
- package/dist/utils/bundledLanguages.d.ts +0 -8
- package/dist/utils/bundledLanguages.js +0 -143
package/dist/theme.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export declare const theme: {
|
|
|
42
42
|
header: string;
|
|
43
43
|
buttons: string;
|
|
44
44
|
language: string;
|
|
45
|
-
skeleton: string;
|
|
46
45
|
pre: string;
|
|
47
46
|
line: string;
|
|
48
47
|
};
|
|
@@ -195,7 +194,6 @@ export declare const shadcnTheme: {
|
|
|
195
194
|
header: string;
|
|
196
195
|
buttons: string;
|
|
197
196
|
language: string;
|
|
198
|
-
skeleton: string;
|
|
199
197
|
pre: string;
|
|
200
198
|
line: string;
|
|
201
199
|
};
|
|
@@ -353,7 +351,6 @@ export declare const mergeTheme: (customTheme?: DeepPartialTheme, baseTheme?: "t
|
|
|
353
351
|
header: string;
|
|
354
352
|
buttons: string;
|
|
355
353
|
language: string;
|
|
356
|
-
skeleton: string;
|
|
357
354
|
pre: string;
|
|
358
355
|
line: string;
|
|
359
356
|
};
|
package/dist/theme.js
CHANGED
|
@@ -43,7 +43,6 @@ export const theme = {
|
|
|
43
43
|
header: 'flex items-center justify-between bg-gray-100/80 p-2 text-gray-600 text-xs',
|
|
44
44
|
buttons: 'flex items-center gap-2',
|
|
45
45
|
language: 'ml-1 font-mono lowercase',
|
|
46
|
-
skeleton: 'block rounded-md font-mono text-transparent bg-gray-200 scale-y-90 animate-pulse whitespace-nowrap',
|
|
47
46
|
pre: 'overflow-x-auto font-mono p-0 bg-gray-100/40',
|
|
48
47
|
line: 'block'
|
|
49
48
|
},
|
|
@@ -196,7 +195,6 @@ export const shadcnTheme = {
|
|
|
196
195
|
header: 'flex items-center justify-between bg-muted/80 px-2 py-1 text-muted-foreground text-xs',
|
|
197
196
|
buttons: 'flex items-center gap-2',
|
|
198
197
|
language: 'ml-1 font-mono lowercase',
|
|
199
|
-
skeleton: 'block rounded-md font-mono text-transparent bg-border/80 scale-y-90 w-fit animate-pulse whitespace-nowrap',
|
|
200
198
|
pre: 'overflow-x-auto font-mono p-0 bg-muted/40',
|
|
201
199
|
line: 'block '
|
|
202
200
|
},
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HighlightTheme } from '@tanstack/highlight/theme';
|
|
2
|
+
export declare const DEFAULT_THEMES: Record<string, HighlightTheme>;
|
|
3
|
+
/** Resolves the active theme: explicit key, else the first user theme, else the dark-mode default. */
|
|
4
|
+
export declare function resolveHighlightTheme(key: string | undefined, extra: Record<string, HighlightTheme> | undefined, isDark: boolean): HighlightTheme;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import githubDark from '@tanstack/highlight/themes/github-dark';
|
|
2
|
+
import githubLight from '@tanstack/highlight/themes/github-light';
|
|
3
|
+
// ponytail: themes live apart from the highlighter so <Streamdown> never pulls in the
|
|
4
|
+
// language scanners — those are only reachable from the opt-in <Code> component.
|
|
5
|
+
export const DEFAULT_THEMES = {
|
|
6
|
+
'github-dark': githubDark,
|
|
7
|
+
'github-light': githubLight
|
|
8
|
+
};
|
|
9
|
+
/** Resolves the active theme: explicit key, else the first user theme, else the dark-mode default. */
|
|
10
|
+
export function resolveHighlightTheme(key, extra, isDark) {
|
|
11
|
+
const themes = { ...DEFAULT_THEMES, ...extra };
|
|
12
|
+
const fallback = DEFAULT_THEMES[isDark ? 'github-dark' : 'github-light'];
|
|
13
|
+
return themes[key ?? (extra ? Object.keys(extra)[0] : fallback.name)] ?? fallback;
|
|
14
|
+
}
|
|
@@ -1,30 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
declare
|
|
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[]);
|
|
13
|
-
private loadHighlighter;
|
|
14
|
-
private isThemeAvailable;
|
|
15
|
-
private loadLanguage;
|
|
16
|
-
private isLanguageSupported;
|
|
17
|
-
isReady(theme: string, language: string | undefined): boolean;
|
|
18
|
-
/**
|
|
19
|
-
* Ensures the highlighter is ready for the given theme and language.
|
|
20
|
-
*/
|
|
21
|
-
load(theme: string, language: string | undefined): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Highlights code synchronously. Must call isReady() first.
|
|
24
|
-
* Returns plaintext tokens for unsupported languages.
|
|
25
|
-
*/
|
|
26
|
-
highlightCode(code: string, language: string | undefined, theme: string): ThemedToken[][];
|
|
27
|
-
static create(languages?: LanguageInfo[], additionalThemes?: Record<string, ThemeRegistration>, additionalLanguages?: LanguageInfo[]): HighlighterManager;
|
|
28
|
-
}
|
|
29
|
-
export { HighlighterManager };
|
|
1
|
+
import { type HighlightToken, type LanguageDefinition } from '@tanstack/highlight';
|
|
2
|
+
/**
|
|
3
|
+
* Tokenizes code into lines of tokens. Synchronous and isomorphic: works during SSR.
|
|
4
|
+
* Unknown languages fall back to plaintext.
|
|
5
|
+
*/
|
|
6
|
+
export declare function highlightLines(code: string, lang: string | undefined, extra?: LanguageDefinition[]): HighlightToken[][];
|
|
30
7
|
export declare const languageExtensionMap: Record<string, string>;
|
|
@@ -1,219 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'github-dark': githubDark,
|
|
11
|
-
'github-light': githubLight
|
|
12
|
-
};
|
|
13
|
-
class HighlighterManager {
|
|
14
|
-
loadedLanguages = new SvelteMap();
|
|
15
|
-
highlighter = $state(null);
|
|
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
|
-
}
|
|
39
|
-
if (typeof window !== 'undefined') {
|
|
40
|
-
Object.assign(window, {
|
|
41
|
-
STREAMDOWN_HIGHLIGHTER: this
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
async loadHighlighter() {
|
|
46
|
-
if (this.highlighter instanceof Promise) {
|
|
47
|
-
return this.highlighter;
|
|
48
|
-
}
|
|
49
|
-
else if (!this.highlighter) {
|
|
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,
|
|
60
|
-
langs: [],
|
|
61
|
-
engine
|
|
62
|
-
});
|
|
63
|
-
this.highlighter = highlighter;
|
|
64
|
-
resolve(highlighter);
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
reject(error);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
return this.highlighter;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
return this.highlighter;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
isThemeAvailable(theme) {
|
|
77
|
-
return theme in DEFAULT_THEMES || theme in this.additionalThemes;
|
|
1
|
+
import { createHighlighter, defaultHighlighter, allLanguages } from '@tanstack/highlight';
|
|
2
|
+
const highlighters = new WeakMap();
|
|
3
|
+
const getHighlighter = (extra) => {
|
|
4
|
+
if (!extra?.length)
|
|
5
|
+
return defaultHighlighter;
|
|
6
|
+
let highlighter = highlighters.get(extra);
|
|
7
|
+
if (!highlighter) {
|
|
8
|
+
highlighter = createHighlighter({ languages: [...allLanguages, ...extra] });
|
|
9
|
+
highlighters.set(extra, highlighter);
|
|
78
10
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.then((langModule) => {
|
|
97
|
-
const langObj = langModule.default || langModule;
|
|
98
|
-
return highlighter.loadLanguage(langObj);
|
|
99
|
-
})
|
|
100
|
-
.then(() => {
|
|
101
|
-
this.loadedLanguages.set(language, true);
|
|
102
|
-
})
|
|
103
|
-
.catch((err) => {
|
|
104
|
-
this.loadedLanguages.set(language, false);
|
|
105
|
-
throw err;
|
|
106
|
-
});
|
|
107
|
-
this.loadedLanguages.set(language, languageLoaderPromise);
|
|
108
|
-
await languageLoaderPromise;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
isLanguageSupported = (language) => {
|
|
112
|
-
return this.customLanguages.has(language);
|
|
113
|
-
};
|
|
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
|
-
}
|
|
123
|
-
return (!!this.highlighter &&
|
|
124
|
-
!(this.highlighter instanceof Promise) &&
|
|
125
|
-
this.loadedLanguages.get(language) === true);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Ensures the highlighter is ready for the given theme and language.
|
|
129
|
-
*/
|
|
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);
|
|
140
|
-
if (this.highlighter !== null &&
|
|
141
|
-
!(this.highlighter instanceof Promise) &&
|
|
142
|
-
languageLoader === true) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const highlighter = await this.loadHighlighter();
|
|
146
|
-
await this.loadLanguage(language, highlighter);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Highlights code synchronously. Must call isReady() first.
|
|
150
|
-
* Returns plaintext tokens for unsupported languages.
|
|
151
|
-
*/
|
|
152
|
-
highlightCode(code, language, theme) {
|
|
153
|
-
try {
|
|
154
|
-
const highlighter = this.highlighter;
|
|
155
|
-
if (!highlighter || highlighter instanceof Promise) {
|
|
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
|
-
]);
|
|
174
|
-
}
|
|
175
|
-
const tokens = highlighter.codeToTokensBase(code, {
|
|
176
|
-
lang: language,
|
|
177
|
-
theme
|
|
178
|
-
});
|
|
179
|
-
return tokens;
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
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
|
-
]);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
static create(languages = bundledLanguagesInfo, additionalThemes, additionalLanguages) {
|
|
193
|
-
if (typeof window !== 'undefined' && 'STREAMDOWN_HIGHLIGHTER' in window) {
|
|
194
|
-
const previousHighlighter = window.STREAMDOWN_HIGHLIGHTER;
|
|
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
|
-
}
|
|
210
|
-
return previousHighlighter;
|
|
11
|
+
return highlighter;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Tokenizes code into lines of tokens. Synchronous and isomorphic: works during SSR.
|
|
15
|
+
* Unknown languages fall back to plaintext.
|
|
16
|
+
*/
|
|
17
|
+
export function highlightLines(code, lang, extra) {
|
|
18
|
+
const tokens = getHighlighter(extra).tokenize(code, { lang }).tokens;
|
|
19
|
+
const lines = [[]];
|
|
20
|
+
for (const token of tokens) {
|
|
21
|
+
const parts = token.value.split('\n');
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
if (i > 0)
|
|
24
|
+
lines.push([]);
|
|
25
|
+
// ponytail: empty segments carry no text, so skip them instead of emitting empty spans
|
|
26
|
+
if (parts[i])
|
|
27
|
+
lines[lines.length - 1].push({ ...token, value: parts[i] });
|
|
211
28
|
}
|
|
212
|
-
return new HighlighterManager(languages, additionalThemes, additionalLanguages);
|
|
213
29
|
}
|
|
30
|
+
return lines;
|
|
214
31
|
}
|
|
215
|
-
// Export the class for those who want to create their own instances
|
|
216
|
-
export { HighlighterManager };
|
|
217
32
|
export const languageExtensionMap = {
|
|
218
33
|
'1c': '1c',
|
|
219
34
|
'1c-query': '1cq',
|
|
@@ -98,6 +98,20 @@ export class IncompleteMarkdownParser {
|
|
|
98
98
|
// Create default plugins that replicate the original handler functions
|
|
99
99
|
static createDefaultPlugins() {
|
|
100
100
|
return [
|
|
101
|
+
{
|
|
102
|
+
// Runs first: in a list item a '>' before a number is a comparison
|
|
103
|
+
// ('- > 25: rich'), but marked reads it as a nested blockquote. Escaping it
|
|
104
|
+
// keeps the text, and the escape renders as a plain '>' (4fffb9f).
|
|
105
|
+
// `pattern` is only a cheap gate — a line whose first non-space character is
|
|
106
|
+
// not a list marker can never match, and bails on that one character.
|
|
107
|
+
name: 'comparisonOperator',
|
|
108
|
+
pattern: /^\s*[-*+\d]/,
|
|
109
|
+
skipInBlockTypes: ['code', 'math'],
|
|
110
|
+
handler: ({ line }) => {
|
|
111
|
+
const match = listItemComparison.exec(line);
|
|
112
|
+
return match ? `${match[1]}\\>${line.slice(match[0].length)}` : line;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
101
115
|
// Block-level plugin that manages blocking contexts
|
|
102
116
|
{
|
|
103
117
|
name: 'contextManager',
|
|
@@ -172,7 +186,9 @@ export class IncompleteMarkdownParser {
|
|
|
172
186
|
result += '\n```';
|
|
173
187
|
}
|
|
174
188
|
if (state.blockingContexts.has('math')) {
|
|
175
|
-
|
|
189
|
+
// The first half of the closing '$$' may already have arrived: adding a
|
|
190
|
+
// whole '\n$$' would leave a stray '$' inside the math and '$$' after it.
|
|
191
|
+
result += result.endsWith('$') && !result.endsWith('$$') ? '$' : '\n$$';
|
|
176
192
|
}
|
|
177
193
|
if (state.blockingContexts.has('center')) {
|
|
178
194
|
result += '\n[/center]';
|
|
@@ -195,6 +211,8 @@ export class IncompleteMarkdownParser {
|
|
|
195
211
|
const tripleAsterisks = (line.match(/\*\*\*/g) || []).length;
|
|
196
212
|
if (tripleAsterisks % 2 === 1) {
|
|
197
213
|
const lastTripleAsteriskIndex = line.lastIndexOf('***');
|
|
214
|
+
if (isWithinCompleteInlineCode(line, lastTripleAsteriskIndex))
|
|
215
|
+
return line;
|
|
198
216
|
const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastTripleAsteriskIndex);
|
|
199
217
|
if (isEndingWithTripleAsterisk) {
|
|
200
218
|
return line.substring(0, lastTripleAsteriskIndex);
|
|
@@ -222,6 +240,8 @@ export class IncompleteMarkdownParser {
|
|
|
222
240
|
if (doubleAsteriskMatches % 2 === 1) {
|
|
223
241
|
const isEndingWithDoubleAsterisk = line.endsWith('**');
|
|
224
242
|
const lastDoubleAsteriskIndex = line.lastIndexOf('**');
|
|
243
|
+
if (isWithinCompleteInlineCode(line, lastDoubleAsteriskIndex))
|
|
244
|
+
return line;
|
|
225
245
|
const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastDoubleAsteriskIndex);
|
|
226
246
|
if (isEndingWithDoubleAsterisk) {
|
|
227
247
|
return line.substring(0, lastDoubleAsteriskIndex);
|
|
@@ -248,6 +268,8 @@ export class IncompleteMarkdownParser {
|
|
|
248
268
|
if (underscorePairs % 2 === 1) {
|
|
249
269
|
const isEndingWithDoubleUnderscore = line.endsWith('__');
|
|
250
270
|
const lastDoubleUnderscoreIndex = line.lastIndexOf('__');
|
|
271
|
+
if (isWithinCompleteInlineCode(line, lastDoubleUnderscoreIndex))
|
|
272
|
+
return line;
|
|
251
273
|
const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastDoubleUnderscoreIndex);
|
|
252
274
|
if (isEndingWithDoubleUnderscore) {
|
|
253
275
|
return line.substring(0, lastDoubleUnderscoreIndex);
|
|
@@ -270,6 +292,8 @@ export class IncompleteMarkdownParser {
|
|
|
270
292
|
if (tildePairs % 2 === 1) {
|
|
271
293
|
const isEndingWithDoubleTilde = line.endsWith('~~');
|
|
272
294
|
const lastDoubleTildeIndex = line.lastIndexOf('~~');
|
|
295
|
+
if (isWithinCompleteInlineCode(line, lastDoubleTildeIndex))
|
|
296
|
+
return line;
|
|
273
297
|
const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastDoubleTildeIndex);
|
|
274
298
|
// Only complete if there's content after the tildes
|
|
275
299
|
const contentAfterTildes = line.substring(lastDoubleTildeIndex + 2, endOfCellOrLine);
|
|
@@ -296,37 +320,43 @@ export class IncompleteMarkdownParser {
|
|
|
296
320
|
}
|
|
297
321
|
// Inline countSingleAsterisks logic
|
|
298
322
|
let singleAsterisks = 0;
|
|
323
|
+
let lastSingleAsterisk = -1;
|
|
299
324
|
for (let i = 0; i < line.length; i++) {
|
|
300
325
|
if (line[i] === '*') {
|
|
301
326
|
const prevChar = i > 0 ? line[i - 1] : '';
|
|
302
327
|
const nextChar = i < line.length - 1 ? line[i + 1] : '';
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
if (j === 0) {
|
|
310
|
-
lineStartIndex = 0;
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
328
|
+
// Whitespace on both sides means arithmetic ('5 * 0') or a bare list
|
|
329
|
+
// marker, never an emphasis delimiter (c347b53).
|
|
330
|
+
if (isSpaceOrEdge(prevChar) && isSpaceOrEdge(nextChar)) {
|
|
331
|
+
continue;
|
|
313
332
|
}
|
|
314
|
-
|
|
315
|
-
if (beforeAsterisk.trim() === '' && (nextChar === ' ' || nextChar === '\t')) {
|
|
333
|
+
if (isWithinCompleteInlineCode(line, i)) {
|
|
316
334
|
continue;
|
|
317
335
|
}
|
|
318
336
|
if (prevChar !== '*' && nextChar !== '*') {
|
|
319
337
|
singleAsterisks++;
|
|
338
|
+
lastSingleAsterisk = i;
|
|
320
339
|
}
|
|
321
340
|
}
|
|
322
341
|
}
|
|
323
342
|
if (singleAsterisks % 2 === 1) {
|
|
343
|
+
// The dangling asterisk is the last counted one. If it cannot OPEN
|
|
344
|
+
// emphasis (nothing or whitespace after it) it is an intraword or
|
|
345
|
+
// trailing closer — '*foo*bar*' is literal, not half of '*foo*bar**'
|
|
346
|
+
// (9f96409).
|
|
347
|
+
if (isSpaceOrEdge(line[lastSingleAsterisk + 1] ?? '')) {
|
|
348
|
+
return line;
|
|
349
|
+
}
|
|
324
350
|
// Inline findFirstSingleAsterisk logic
|
|
325
351
|
let firstSingleAsteriskIndex = -1;
|
|
326
352
|
for (let i = 0; i < line.length; i++) {
|
|
327
353
|
if (line[i] === '*' && line[i - 1] !== '*' && line[i + 1] !== '*') {
|
|
328
354
|
const prevChar = i > 0 ? line[i - 1] : '';
|
|
329
355
|
const nextChar = i < line.length - 1 ? line[i + 1] : '';
|
|
356
|
+
if (isSpaceOrEdge(prevChar) && isSpaceOrEdge(nextChar))
|
|
357
|
+
continue;
|
|
358
|
+
if (isWithinCompleteInlineCode(line, i))
|
|
359
|
+
continue;
|
|
330
360
|
if (/\w/.test(prevChar) && /\w/.test(nextChar))
|
|
331
361
|
continue;
|
|
332
362
|
if (/\w/.test(prevChar) && !/\s/.test(prevChar))
|
|
@@ -391,6 +421,8 @@ export class IncompleteMarkdownParser {
|
|
|
391
421
|
continue;
|
|
392
422
|
if (isWithinMathBlock(line, i))
|
|
393
423
|
continue;
|
|
424
|
+
if (isWithinCompleteInlineCode(line, i))
|
|
425
|
+
continue;
|
|
394
426
|
if (prevChar &&
|
|
395
427
|
nextChar &&
|
|
396
428
|
/[\p{L}\p{N}_]/u.test(prevChar) &&
|
|
@@ -410,7 +442,8 @@ export class IncompleteMarkdownParser {
|
|
|
410
442
|
line[i - 1] !== '_' &&
|
|
411
443
|
line[i + 1] !== '_' &&
|
|
412
444
|
line[i - 1] !== '\\' &&
|
|
413
|
-
!isWithinMathBlock(line, i)
|
|
445
|
+
!isWithinMathBlock(line, i) &&
|
|
446
|
+
!isWithinCompleteInlineCode(line, i)) {
|
|
414
447
|
const prevChar = i > 0 ? line[i - 1] : '';
|
|
415
448
|
const nextChar = i < line.length - 1 ? line[i + 1] : '';
|
|
416
449
|
if (prevChar &&
|
|
@@ -450,11 +483,17 @@ export class IncompleteMarkdownParser {
|
|
|
450
483
|
}
|
|
451
484
|
if (singleTildes % 2 === 1) {
|
|
452
485
|
const lastTildeIndex = line.lastIndexOf('~');
|
|
453
|
-
if (lastTildeIndex !== -1 &&
|
|
486
|
+
if (lastTildeIndex !== -1 &&
|
|
487
|
+
!isWithinMathBlock(line, lastTildeIndex) &&
|
|
488
|
+
!isWithinCompleteInlineCode(line, lastTildeIndex)) {
|
|
454
489
|
const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastTildeIndex);
|
|
455
|
-
// Only complete if there's content after the tilde
|
|
456
490
|
const contentAfterTilde = line.substring(lastTildeIndex + 1, endOfCellOrLine);
|
|
457
|
-
|
|
491
|
+
// A subscript is '~text~' with no whitespace inside (same rule as the
|
|
492
|
+
// lexer), and a digit before the tilde means a range like '20~25°C':
|
|
493
|
+
// closing those would manufacture a subscript nobody typed (716a5f0).
|
|
494
|
+
if (contentAfterTilde.length > 0 &&
|
|
495
|
+
!/\s/.test(contentAfterTilde) &&
|
|
496
|
+
!/\d/.test(line[lastTildeIndex - 1] ?? '')) {
|
|
458
497
|
return line.substring(0, endOfCellOrLine) + '~' + line.substring(endOfCellOrLine);
|
|
459
498
|
}
|
|
460
499
|
}
|
|
@@ -580,6 +619,9 @@ export class IncompleteMarkdownParser {
|
|
|
580
619
|
continue;
|
|
581
620
|
if (nextChar && /\d/.test(nextChar))
|
|
582
621
|
continue;
|
|
622
|
+
// A '$' shown as code ('`$var`') must not flip the parity (e50b0c4)
|
|
623
|
+
if (isWithinCompleteInlineCode(line, i))
|
|
624
|
+
continue;
|
|
583
625
|
singleDollars++;
|
|
584
626
|
}
|
|
585
627
|
}
|
|
@@ -593,7 +635,8 @@ export class IncompleteMarkdownParser {
|
|
|
593
635
|
prevChar !== '$' &&
|
|
594
636
|
nextChar !== '$' &&
|
|
595
637
|
nextChar !== '' &&
|
|
596
|
-
!/\d/.test(nextChar)
|
|
638
|
+
!/\d/.test(nextChar) &&
|
|
639
|
+
!isWithinCompleteInlineCode(line, i)) {
|
|
597
640
|
lastDollarIndex = i;
|
|
598
641
|
break;
|
|
599
642
|
}
|
|
@@ -866,6 +909,11 @@ export const parseIncompleteMarkdown = (text) => {
|
|
|
866
909
|
return defaultParser.parse(text);
|
|
867
910
|
};
|
|
868
911
|
// Utility functions
|
|
912
|
+
// Full test for the comparisonOperator plugin, whose `pattern` only gates it.
|
|
913
|
+
const listItemComparison = /^(\s*(?:[-*+]|\d+[.)]) +)>(?==?\s*\$?\d)/;
|
|
914
|
+
// The char accessors in the plugins return '' past either end of the line, so an
|
|
915
|
+
// empty string here means "edge of line".
|
|
916
|
+
const isSpaceOrEdge = (char) => !char || /\s/.test(char);
|
|
869
917
|
const findEndOfCellOrLineContaining = (text, position) => {
|
|
870
918
|
let endPos = position;
|
|
871
919
|
while (endPos < text.length && text[endPos] !== '\n' && text[endPos] !== '|') {
|
|
@@ -873,6 +921,40 @@ const findEndOfCellOrLineContaining = (text, position) => {
|
|
|
873
921
|
}
|
|
874
922
|
return endPos;
|
|
875
923
|
};
|
|
924
|
+
// Scanned on demand, against the line as it stands now, and allocating nothing:
|
|
925
|
+
// the completer runs over every changed block of every streamed chunk, so garbage
|
|
926
|
+
// here surfaces as GC pauses in the stages around it.
|
|
927
|
+
const isWithinCompleteInlineCode = (line, position) => {
|
|
928
|
+
let open = line.indexOf('`');
|
|
929
|
+
while (open !== -1 && open <= position) {
|
|
930
|
+
let openEnd = open;
|
|
931
|
+
while (line.charCodeAt(openEnd) === 96)
|
|
932
|
+
openEnd++;
|
|
933
|
+
const runLength = openEnd - open;
|
|
934
|
+
let closeStart = -1;
|
|
935
|
+
for (let j = openEnd; j < line.length; j++) {
|
|
936
|
+
if (line.charCodeAt(j) !== 96)
|
|
937
|
+
continue;
|
|
938
|
+
let closeEnd = j;
|
|
939
|
+
while (line.charCodeAt(closeEnd) === 96)
|
|
940
|
+
closeEnd++;
|
|
941
|
+
if (closeEnd - j === runLength) {
|
|
942
|
+
closeStart = j;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
j = closeEnd - 1;
|
|
946
|
+
}
|
|
947
|
+
// An unterminated run closes nothing, so neither it nor anything after it
|
|
948
|
+
// is code: completing emphasis inside it is what streaming needs.
|
|
949
|
+
if (closeStart === -1)
|
|
950
|
+
return false;
|
|
951
|
+
const spanEnd = closeStart + runLength;
|
|
952
|
+
if (position < spanEnd)
|
|
953
|
+
return true;
|
|
954
|
+
open = line.indexOf('`', spanEnd);
|
|
955
|
+
}
|
|
956
|
+
return false;
|
|
957
|
+
};
|
|
876
958
|
const isWithinMathBlock = (text, position) => {
|
|
877
959
|
let inInlineMath = false;
|
|
878
960
|
let inBlockMath = false;
|
package/dist/utils/save.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export const save = (filename, content, mimeType) => {
|
|
2
|
-
|
|
2
|
+
// Excel on Windows assumes the system codepage without a BOM, so accented
|
|
3
|
+
// and CJK text in a downloaded CSV opens as mojibake.
|
|
4
|
+
const body = mimeType.startsWith('text/csv') ? '\uFEFF' + content : content;
|
|
5
|
+
const blob = new Blob([body], { type: mimeType });
|
|
3
6
|
const url = URL.createObjectURL(blob);
|
|
4
7
|
const link = document.createElement('a');
|
|
5
8
|
link.href = url;
|
package/dist/utils/url.js
CHANGED
|
@@ -25,6 +25,7 @@ export const isPathRelativeUrl = (url) => {
|
|
|
25
25
|
return false;
|
|
26
26
|
return url.startsWith('/');
|
|
27
27
|
};
|
|
28
|
+
const WILDCARD_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
28
29
|
export const transformUrl = (url, allowedPrefixes, defaultOrigin) => {
|
|
29
30
|
if (!url)
|
|
30
31
|
return null;
|
|
@@ -61,8 +62,11 @@ export const transformUrl = (url, allowedPrefixes, defaultOrigin) => {
|
|
|
61
62
|
}
|
|
62
63
|
// Check for wildcard - allow all URLs
|
|
63
64
|
if (allowedPrefixes.includes('*')) {
|
|
64
|
-
//
|
|
65
|
-
|
|
65
|
+
// The wildcard allows the protocols a document can legitimately link to.
|
|
66
|
+
// javascript:, data: and vbscript: stay blocked - they execute in the
|
|
67
|
+
// page's origin. mailto:/tel: were missing, so the default ['*'] config
|
|
68
|
+
// rendered a [blocked] badge on every phone and email link.
|
|
69
|
+
if (!WILDCARD_PROTOCOLS.has(parsedUrl.protocol)) {
|
|
66
70
|
return null;
|
|
67
71
|
}
|
|
68
72
|
const inputWasRelative = isPathRelativeUrl(url);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-streamdown",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"packageManager": "pnpm@10.32.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"lint": "prettier --check .",
|
|
23
23
|
"test:ui": "vitest --ui",
|
|
24
24
|
"test:unit": "vitest",
|
|
25
|
-
"test": "
|
|
25
|
+
"test:browser": "vitest --run --project client",
|
|
26
|
+
"test": "npm run test:unit -- --run --project server"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"dist",
|
|
@@ -94,13 +95,11 @@
|
|
|
94
95
|
},
|
|
95
96
|
"dependencies": {
|
|
96
97
|
"@floating-ui/dom": "^1.7.4",
|
|
97
|
-
"@
|
|
98
|
-
"@shikijs/themes": "^3.17.1",
|
|
98
|
+
"@tanstack/highlight": "^0.1.0",
|
|
99
99
|
"clsx": "^2.1.1",
|
|
100
100
|
"katex": "^0.16.22",
|
|
101
|
-
"marked": "
|
|
101
|
+
"marked": "18.0.12",
|
|
102
102
|
"mermaid": "^11.15.0",
|
|
103
|
-
"shiki": "^3.13.0",
|
|
104
103
|
"tailwind-merge": "^3.3.1"
|
|
105
104
|
}
|
|
106
105
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
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>;
|