svelte-streamdown 3.1.2 → 4.0.0
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 +116 -37
- package/dist/Elements/Code.svelte +12 -50
- package/dist/Streamdown.svelte +12 -15
- package/dist/context.svelte.d.ts +9 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/marked/index.d.ts +12 -0
- package/dist/marked/index.js +120 -35
- package/dist/marked/marked-br.js +6 -0
- package/dist/marked/marked-citations.js +6 -0
- package/dist/marked/marked-footnotes.js +12 -2
- package/dist/marked/marked-math.js +5 -0
- package/dist/marked/marked-subsup.js +8 -0
- 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/package.json +3 -5
- package/dist/utils/bundledLanguages.d.ts +0 -8
- package/dist/utils/bundledLanguages.js +0 -143
package/dist/marked/index.js
CHANGED
|
@@ -106,21 +106,79 @@ export const lex = (markdown, extensions = []) => {
|
|
|
106
106
|
export const createParseBlocksCache = () => ({
|
|
107
107
|
content: '',
|
|
108
108
|
raws: [],
|
|
109
|
-
keep: []
|
|
109
|
+
keep: [],
|
|
110
|
+
offsets: [0],
|
|
111
|
+
keptBefore: [0],
|
|
112
|
+
blocks: []
|
|
110
113
|
});
|
|
111
114
|
// Number of trailing rendered blocks that stay "live" (re-lexed every chunk).
|
|
112
115
|
// 2 covers constructs that merge backward as they stream in — e.g. a paragraph
|
|
113
116
|
// line becoming a table once its delimiter row arrives, or a setext heading.
|
|
114
117
|
const SEAL_SLACK = 2;
|
|
115
|
-
|
|
118
|
+
// Spot checks over the sealed prefix per append (see `appendable`). The sealed
|
|
119
|
+
// region was compared byte-for-byte while it was live, so this only has to catch
|
|
120
|
+
// a caller that swapped in a different, longer document.
|
|
121
|
+
const SEAL_PROBES = 16;
|
|
122
|
+
/**
|
|
123
|
+
* A Lexer used only to slice a document into top-level block raws.
|
|
124
|
+
*
|
|
125
|
+
* `Tokenizer.list()` recurses into every list item's text purely to decide
|
|
126
|
+
* whether the list is loose — `raw` is already final by then — and it marks that
|
|
127
|
+
* recursion by setting `state.top = false` first. `Tokenizer.blockquote()` also
|
|
128
|
+
* recurses, but its `raw` genuinely depends on the result (continuation lines are
|
|
129
|
+
* folded into a nested list/blockquote token), and it sets `state.top = true`.
|
|
130
|
+
* So dropping every nested call made with `top === false` removes the whole
|
|
131
|
+
* list-item subtree parse without changing a single raw.
|
|
132
|
+
*/
|
|
133
|
+
class SplitLexer extends Lexer {
|
|
134
|
+
blockTokens(src, tokens = [], lastParagraphClipped = false) {
|
|
135
|
+
if (!this.state.top) {
|
|
136
|
+
// Lexer.blockTokens() sets state.top = true on exit; the skip must too.
|
|
137
|
+
this.state.top = true;
|
|
138
|
+
return tokens;
|
|
139
|
+
}
|
|
140
|
+
return super.blockTokens(src, tokens, lastParagraphClipped);
|
|
141
|
+
}
|
|
142
|
+
/** Extensions (dl, table) tokenize cell/term text inline; raws never depend on it. */
|
|
143
|
+
inlineTokens(src, tokens = []) {
|
|
144
|
+
return tokens;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const blockTokensOf = (markdown, extensions) => new SplitLexer(getBlockOptions(extensions)).blockTokens(markdown, []);
|
|
148
|
+
/**
|
|
149
|
+
* Is `markdown` an append to `cache.content`?
|
|
150
|
+
*
|
|
151
|
+
* The live region [offset, cache.content.length) is compared exactly — that is
|
|
152
|
+
* the only region whose segmentation can still change. The sealed prefix gets
|
|
153
|
+
* SEAL_PROBES sampled character comparisons instead of a full scan: a complete
|
|
154
|
+
* `markdown.startsWith(cache.content)` was 93% of parseBlocks' streaming cost
|
|
155
|
+
* (676 ms of 727 ms over a 100 KB / 5028-chunk stream) and is the single reason
|
|
156
|
+
* the append path was O(N) per chunk rather than O(tail).
|
|
157
|
+
*
|
|
158
|
+
* Note this compares source against source, not source against `raws`: marked's
|
|
159
|
+
* tokenizers normalize (e.g. the list tokenizer rewrites a trailing space as a
|
|
160
|
+
* newline), so raws are not always literal slices of the input.
|
|
161
|
+
*/
|
|
162
|
+
const appendable = (markdown, content, offset) => {
|
|
163
|
+
for (let i = offset; i < content.length; i++) {
|
|
164
|
+
if (markdown.charCodeAt(i) !== content.charCodeAt(i))
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (offset === 0)
|
|
168
|
+
return true;
|
|
169
|
+
const step = offset > SEAL_PROBES ? offset / SEAL_PROBES : 1;
|
|
170
|
+
for (let f = 0; f < offset; f += step) {
|
|
171
|
+
const i = f | 0;
|
|
172
|
+
if (markdown.charCodeAt(i) !== content.charCodeAt(i))
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return markdown.charCodeAt(offset - 1) === content.charCodeAt(offset - 1);
|
|
176
|
+
};
|
|
116
177
|
export const parseBlocks = (markdown, extensions = [], cache) => {
|
|
117
|
-
if (cache &&
|
|
118
|
-
cache.content.length > 0 &&
|
|
119
|
-
markdown.length > cache.content.length &&
|
|
120
|
-
markdown.startsWith(cache.content)) {
|
|
178
|
+
if (cache && cache.content.length > 0 && markdown.length > cache.content.length) {
|
|
121
179
|
// Append-only update: seal everything except the last SEAL_SLACK rendered
|
|
122
|
-
// blocks and re-lex only the tail.
|
|
123
|
-
//
|
|
180
|
+
// blocks and re-lex only the tail. offsets[] are prefix sums over raws, so
|
|
181
|
+
// the seal point costs one array read instead of a running sum.
|
|
124
182
|
let cut = cache.raws.length;
|
|
125
183
|
let liveBlocks = 0;
|
|
126
184
|
while (cut > 0 && liveBlocks < SEAL_SLACK) {
|
|
@@ -128,45 +186,72 @@ export const parseBlocks = (markdown, extensions = [], cache) => {
|
|
|
128
186
|
if (cache.keep[cut])
|
|
129
187
|
liveBlocks++;
|
|
130
188
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
cache.
|
|
145
|
-
cache.
|
|
189
|
+
const offset = cache.offsets[cut];
|
|
190
|
+
if (appendable(markdown, cache.content, offset)) {
|
|
191
|
+
const tailTokens = blockTokensOf(markdown.slice(offset), extensions);
|
|
192
|
+
let tailLength = 0;
|
|
193
|
+
for (const token of tailTokens)
|
|
194
|
+
tailLength += token.raw.length;
|
|
195
|
+
// Contiguity guard: if the lexer normalized the tail (so raws no longer
|
|
196
|
+
// reconstruct the input), the offsets cannot be trusted — full reparse.
|
|
197
|
+
if (offset + tailLength === markdown.length) {
|
|
198
|
+
const keptAtCut = cache.keptBefore[cut];
|
|
199
|
+
cache.raws.length = cut;
|
|
200
|
+
cache.keep.length = cut;
|
|
201
|
+
cache.offsets.length = cut + 1;
|
|
202
|
+
cache.keptBefore.length = cut + 1;
|
|
203
|
+
cache.blocks.length = keptAtCut;
|
|
204
|
+
let pos = offset;
|
|
205
|
+
let kept = keptAtCut;
|
|
206
|
+
for (const token of tailTokens) {
|
|
207
|
+
const raw = token.raw;
|
|
208
|
+
const keep = token.type !== 'space' && token.type !== 'footnote';
|
|
209
|
+
cache.raws.push(raw);
|
|
210
|
+
cache.keep.push(keep);
|
|
211
|
+
pos += raw.length;
|
|
212
|
+
cache.offsets.push(pos);
|
|
213
|
+
if (keep) {
|
|
214
|
+
cache.blocks.push(raw);
|
|
215
|
+
kept++;
|
|
216
|
+
}
|
|
217
|
+
cache.keptBefore.push(kept);
|
|
218
|
+
}
|
|
219
|
+
cache.content = markdown;
|
|
220
|
+
// Copy out: callers (Svelte `$derived`, the perf harness) diff block
|
|
221
|
+
// lists by identity, so handing back the persistent array would read as
|
|
222
|
+
// "nothing changed". slice() is a memcpy with no per-element callback.
|
|
223
|
+
return cache.blocks.slice();
|
|
146
224
|
}
|
|
147
|
-
cache.content = markdown;
|
|
148
|
-
return cache.raws.filter((_, i) => cache.keep[i]);
|
|
149
225
|
}
|
|
150
226
|
}
|
|
151
227
|
// Full parse (first call, non-append update, or contiguity fallback).
|
|
152
228
|
const tokens = blockTokensOf(markdown, extensions);
|
|
153
|
-
const blocks = [];
|
|
154
229
|
if (cache) {
|
|
155
|
-
cache.raws =
|
|
156
|
-
cache.keep =
|
|
157
|
-
|
|
230
|
+
cache.raws.length = 0;
|
|
231
|
+
cache.keep.length = 0;
|
|
232
|
+
cache.offsets.length = 1;
|
|
233
|
+
cache.keptBefore.length = 1;
|
|
234
|
+
cache.blocks.length = 0;
|
|
235
|
+
let pos = 0;
|
|
236
|
+
let kept = 0;
|
|
158
237
|
for (const token of tokens) {
|
|
238
|
+
const raw = token.raw;
|
|
159
239
|
const keep = token.type !== 'space' && token.type !== 'footnote';
|
|
160
|
-
cache.raws.push(
|
|
240
|
+
cache.raws.push(raw);
|
|
161
241
|
cache.keep.push(keep);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
242
|
+
pos += raw.length;
|
|
243
|
+
cache.offsets.push(pos);
|
|
244
|
+
if (keep) {
|
|
245
|
+
cache.blocks.push(raw);
|
|
246
|
+
kept++;
|
|
247
|
+
}
|
|
248
|
+
cache.keptBefore.push(kept);
|
|
165
249
|
}
|
|
166
250
|
// Only trust the cache for future appends if raws reconstruct the input.
|
|
167
|
-
cache.content =
|
|
168
|
-
return blocks;
|
|
251
|
+
cache.content = pos === markdown.length ? markdown : '';
|
|
252
|
+
return cache.blocks.slice();
|
|
169
253
|
}
|
|
254
|
+
const blocks = [];
|
|
170
255
|
for (const token of tokens) {
|
|
171
256
|
if (token.type !== 'space' && token.type !== 'footnote')
|
|
172
257
|
blocks.push(token.raw);
|
package/dist/marked/marked-br.js
CHANGED
|
@@ -2,6 +2,12 @@ export const markedBr = {
|
|
|
2
2
|
name: 'br',
|
|
3
3
|
level: 'inline',
|
|
4
4
|
tokenizer(src) {
|
|
5
|
+
// marked dispatches every inline extension at every scan position; `start`
|
|
6
|
+
// only clips the text rule, it does not gate the tokenizer. This rule is
|
|
7
|
+
// anchored on a single literal character, so one charCodeAt rejects the
|
|
8
|
+
// ~3.6k non-matching dispatches per 100 KB before the regex engine runs.
|
|
9
|
+
if (src.charCodeAt(0) !== 60 /* < */)
|
|
10
|
+
return undefined;
|
|
5
11
|
// Match HTML <br> tags (with or without closing slash, case insensitive)
|
|
6
12
|
const match = src.match(/^<br\s*\/?>/i);
|
|
7
13
|
if (match) {
|
|
@@ -6,6 +6,12 @@ export const markedCitations = {
|
|
|
6
6
|
return i === -1 ? -1 : i;
|
|
7
7
|
},
|
|
8
8
|
tokenizer(src) {
|
|
9
|
+
// marked dispatches every inline extension at every scan position; `start`
|
|
10
|
+
// only clips the text rule, it does not gate the tokenizer. This rule is
|
|
11
|
+
// anchored on a single literal character, so one charCodeAt rejects the
|
|
12
|
+
// ~3.6k non-matching dispatches per 100 KB before the regex engine runs.
|
|
13
|
+
if (src.charCodeAt(0) !== 91 /* [ */)
|
|
14
|
+
return;
|
|
9
15
|
// Match inline citations like [1], [ref], [1] [2], [ref] [ref2], etc.
|
|
10
16
|
// Requires non-empty bracket contents and spaces between adjacent citation brackets
|
|
11
17
|
const match = src.match(/^\[[^\][]+\](?:\s+\[[^\][]+\])*/);
|
|
@@ -34,9 +34,14 @@ export function markedFootnote() {
|
|
|
34
34
|
name: 'footnote',
|
|
35
35
|
level: 'block',
|
|
36
36
|
tokenizer(src) {
|
|
37
|
-
|
|
37
|
+
// Match FIRST. ensureMaps() calls Svelte's getContext(), which throws
|
|
38
|
+
// (and is caught) whenever tokenizing happens outside component init —
|
|
39
|
+
// 3.3 us per call. Running it before the regex charged that to every
|
|
40
|
+
// block token in the document: 8.3 ms of the 13.0 ms it took to split a
|
|
41
|
+
// 100 KB document into blocks.
|
|
38
42
|
const match = footnoteRegex.exec(src);
|
|
39
43
|
if (match) {
|
|
44
|
+
const maps = ensureMaps(this);
|
|
40
45
|
const [raw, label, text = ''] = match;
|
|
41
46
|
let content = text.split('\n').reduce((acc, curr) => {
|
|
42
47
|
return acc + '\n' + curr.replace(/^[ \t]+/, '');
|
|
@@ -66,9 +71,14 @@ export function markedFootnote() {
|
|
|
66
71
|
name: 'footnoteRef',
|
|
67
72
|
level: 'inline',
|
|
68
73
|
tokenizer(src) {
|
|
69
|
-
|
|
74
|
+
// Same ordering rule as the block tokenizer above: this one is an inline
|
|
75
|
+
// extension, so it is dispatched at every inline scan position — so it also
|
|
76
|
+
// gets a single-character guard before the regex.
|
|
77
|
+
if (src.charCodeAt(0) !== 91 /* [ */)
|
|
78
|
+
return;
|
|
70
79
|
const match = footnoteRefRegex.exec(src);
|
|
71
80
|
if (match) {
|
|
81
|
+
const maps = ensureMaps(this);
|
|
72
82
|
const [raw, label] = match;
|
|
73
83
|
const footnote = maps.footnotes.get(label);
|
|
74
84
|
const token = {
|
|
@@ -20,6 +20,8 @@ export const markedMath = [
|
|
|
20
20
|
name: 'math',
|
|
21
21
|
level: 'block',
|
|
22
22
|
tokenizer(src) {
|
|
23
|
+
if (src.charCodeAt(0) !== 36 /* $ */)
|
|
24
|
+
return;
|
|
23
25
|
const match = src.match(blockRule);
|
|
24
26
|
if (match) {
|
|
25
27
|
// match[2] is multiline format, match[3] is single-line format
|
|
@@ -67,6 +69,9 @@ export const markedMath = [
|
|
|
67
69
|
}
|
|
68
70
|
},
|
|
69
71
|
tokenizer(src) {
|
|
72
|
+
// inlineRule is anchored on `$`; skip the regex otherwise (see marked-br.ts).
|
|
73
|
+
if (src.charCodeAt(0) !== 36 /* $ */)
|
|
74
|
+
return;
|
|
70
75
|
const match = src.match(inlineRule);
|
|
71
76
|
if (match) {
|
|
72
77
|
const content = match[2];
|
|
@@ -8,6 +8,12 @@ export const markedSub = {
|
|
|
8
8
|
return i === -1 ? undefined : i;
|
|
9
9
|
},
|
|
10
10
|
tokenizer(src) {
|
|
11
|
+
// marked dispatches every inline extension at every scan position; `start`
|
|
12
|
+
// only clips the text rule, it does not gate the tokenizer. This rule is
|
|
13
|
+
// anchored on a single literal character, so one charCodeAt rejects the
|
|
14
|
+
// ~3.6k non-matching dispatches per 100 KB before the regex engine runs.
|
|
15
|
+
if (src.charCodeAt(0) !== 126 /* ~ */)
|
|
16
|
+
return;
|
|
11
17
|
const match = src.match(subRule);
|
|
12
18
|
if (match) {
|
|
13
19
|
return {
|
|
@@ -27,6 +33,8 @@ export const markedSup = {
|
|
|
27
33
|
return i === -1 ? undefined : i;
|
|
28
34
|
},
|
|
29
35
|
tokenizer(src) {
|
|
36
|
+
if (src.charCodeAt(0) !== 94 /* ^ */)
|
|
37
|
+
return;
|
|
30
38
|
const match = src.match(supRule);
|
|
31
39
|
if (match) {
|
|
32
40
|
return {
|
|
@@ -376,6 +376,19 @@ function processRows(headerRows, bodyRows, alignment, colCount, lexer, maxColspa
|
|
|
376
376
|
}
|
|
377
377
|
return tokens;
|
|
378
378
|
}
|
|
379
|
+
// Hoisted: this was `new RegExp(...)` inside the tokenizer, so a ~1 KB pattern was
|
|
380
|
+
// concatenated and a RegExp constructed on every block-tokenizer dispatch — 3.3 ms
|
|
381
|
+
// of the 13.0 ms it took to split a 100 KB document into blocks.
|
|
382
|
+
const TABLE_WITH_ALIGN = new RegExp('^' +
|
|
383
|
+
'([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' + // Header
|
|
384
|
+
' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' + // Header Align
|
|
385
|
+
'(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' + // Body Cells
|
|
386
|
+
'(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
|
|
387
|
+
'(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
|
|
388
|
+
'<\\/?(?:address|article|aside|base|basefont|blockquote|body|' +
|
|
389
|
+
'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)');
|
|
390
|
+
// Tables written without a header alignment row (`|a|b|` rows only).
|
|
391
|
+
const TABLE_NO_ALIGN = /^(\|.*\|(?:\n\|.*\|)*)/;
|
|
379
392
|
const { detectFooter, maxColspan } = DEFAULT_OPTIONS;
|
|
380
393
|
// Adds support for extended tables in marked with row spanning, column spanning,
|
|
381
394
|
// multi-row headers, and column alignment
|
|
@@ -395,21 +408,11 @@ export const markedTable = {
|
|
|
395
408
|
},
|
|
396
409
|
tokenizer(src) {
|
|
397
410
|
// Try to match table with header and alignment first
|
|
398
|
-
let
|
|
399
|
-
'([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' + // Header
|
|
400
|
-
' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' + // Header Align
|
|
401
|
-
'(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' + // Body Cells
|
|
402
|
-
'(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
|
|
403
|
-
'(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
|
|
404
|
-
'<\\/?(?:address|article|aside|base|basefont|blockquote|body|' +
|
|
405
|
-
'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)');
|
|
406
|
-
let cap = regex.exec(src);
|
|
411
|
+
let cap = TABLE_WITH_ALIGN.exec(src);
|
|
407
412
|
let hasHeaderAlignment = true;
|
|
408
413
|
// If no match with header alignment, try table without header alignment
|
|
409
414
|
if (!cap) {
|
|
410
|
-
|
|
411
|
-
regex = /^(\|.*\|(?:\n\|.*\|)*)/;
|
|
412
|
-
cap = regex.exec(src);
|
|
415
|
+
cap = TABLE_NO_ALIGN.exec(src);
|
|
413
416
|
hasHeaderAlignment = false;
|
|
414
417
|
}
|
|
415
418
|
if (!cap)
|
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>;
|