@wingleeio/mugen-markdown 0.1.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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/index.cjs +1903 -0
- package/dist/index.d.cts +498 -0
- package/dist/index.d.mts +498 -0
- package/dist/index.mjs +1861 -0
- package/package.json +79 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1903 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let react = require("react");
|
|
3
|
+
let _wingleeio_mugen = require("@wingleeio/mugen");
|
|
4
|
+
let _incremark_core = require("@incremark/core");
|
|
5
|
+
let _chenglou_pretext_rich_inline = require("@chenglou/pretext/rich-inline");
|
|
6
|
+
//#region src/parse.ts
|
|
7
|
+
const DEFAULTS = { gfm: true };
|
|
8
|
+
function optionsKey(opts) {
|
|
9
|
+
return JSON.stringify([
|
|
10
|
+
opts.gfm ?? true,
|
|
11
|
+
typeof opts.math === "object" ? opts.math : opts.math ?? false,
|
|
12
|
+
typeof opts.containers === "object" ? true : opts.containers ?? false
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
function makeParser(opts) {
|
|
16
|
+
return (0, _incremark_core.createIncremarkParser)({
|
|
17
|
+
gfm: opts.gfm ?? true,
|
|
18
|
+
math: opts.math ?? false,
|
|
19
|
+
containers: opts.containers ?? false
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const MAX_AST_CACHE = 512;
|
|
23
|
+
const astCache = /* @__PURE__ */ new Map();
|
|
24
|
+
function readAst(cacheKey) {
|
|
25
|
+
const cached = astCache.get(cacheKey);
|
|
26
|
+
if (cached === void 0) return void 0;
|
|
27
|
+
astCache.delete(cacheKey);
|
|
28
|
+
astCache.set(cacheKey, cached);
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
function writeAst(cacheKey, ast) {
|
|
32
|
+
if (astCache.has(cacheKey)) astCache.delete(cacheKey);
|
|
33
|
+
else if (astCache.size >= MAX_AST_CACHE) {
|
|
34
|
+
const oldest = astCache.keys().next().value;
|
|
35
|
+
if (oldest !== void 0) astCache.delete(oldest);
|
|
36
|
+
}
|
|
37
|
+
astCache.set(cacheKey, ast);
|
|
38
|
+
}
|
|
39
|
+
const MAX_LIVE = 16;
|
|
40
|
+
const live = [];
|
|
41
|
+
/** Find the live parser (same options) whose source is the longest proper prefix of `source`. */
|
|
42
|
+
function findExtensible(key, source) {
|
|
43
|
+
let bestIdx = -1;
|
|
44
|
+
let bestLen = -1;
|
|
45
|
+
for (let i = 0; i < live.length; i++) {
|
|
46
|
+
const e = live[i];
|
|
47
|
+
if (e.key !== key) continue;
|
|
48
|
+
const len = e.lastSource.length;
|
|
49
|
+
if (len < source.length && len > bestLen && source.startsWith(e.lastSource)) {
|
|
50
|
+
bestIdx = i;
|
|
51
|
+
bestLen = len;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return bestIdx;
|
|
55
|
+
}
|
|
56
|
+
function promote(idx) {
|
|
57
|
+
const [entry] = live.splice(idx, 1);
|
|
58
|
+
live.unshift(entry);
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse markdown into an mdast `Root` with incremark, memoized by
|
|
63
|
+
* `(source, options)` and parsed **incrementally** when the source grows.
|
|
64
|
+
*
|
|
65
|
+
* Pure and synchronous — safe to call inside mugen's measure walk. Growing the
|
|
66
|
+
* same source (a streaming row) appends only the new text to a retained parser;
|
|
67
|
+
* an unchanged source is served from the AST cache; a non-extending change parses
|
|
68
|
+
* fresh.
|
|
69
|
+
*/
|
|
70
|
+
function parseMarkdown(source, options = DEFAULTS) {
|
|
71
|
+
const key = optionsKey(options);
|
|
72
|
+
const cacheKey = `${key} ${source}`;
|
|
73
|
+
const cached = readAst(cacheKey);
|
|
74
|
+
if (cached !== void 0) return cached;
|
|
75
|
+
let ast;
|
|
76
|
+
const idx = findExtensible(key, source);
|
|
77
|
+
if (idx >= 0) {
|
|
78
|
+
const entry = promote(idx);
|
|
79
|
+
const delta = source.slice(entry.lastSource.length);
|
|
80
|
+
ast = entry.parser.append(delta).ast;
|
|
81
|
+
entry.lastSource = source;
|
|
82
|
+
} else {
|
|
83
|
+
const parser = makeParser(options);
|
|
84
|
+
ast = parser.append(source).ast;
|
|
85
|
+
live.unshift({
|
|
86
|
+
key,
|
|
87
|
+
parser,
|
|
88
|
+
lastSource: source
|
|
89
|
+
});
|
|
90
|
+
if (live.length > MAX_LIVE) live.pop();
|
|
91
|
+
}
|
|
92
|
+
writeAst(cacheKey, ast);
|
|
93
|
+
return ast;
|
|
94
|
+
}
|
|
95
|
+
/** Drop the parse caches and retained parsers (tests / memory pressure). */
|
|
96
|
+
function clearParseCache() {
|
|
97
|
+
astCache.clear();
|
|
98
|
+
live.length = 0;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/highlight/types.ts
|
|
102
|
+
/**
|
|
103
|
+
* Default palette: mid-tone colours chosen to stay legible on both light and
|
|
104
|
+
* dark page backgrounds, since the default theme inherits the page colours.
|
|
105
|
+
*/
|
|
106
|
+
const defaultTokenColors = {
|
|
107
|
+
keyword: "#a855f7",
|
|
108
|
+
string: "#059669",
|
|
109
|
+
comment: "#8a919e",
|
|
110
|
+
number: "#d97706",
|
|
111
|
+
constant: "#ea580c",
|
|
112
|
+
function: "#3b82f6",
|
|
113
|
+
type: "#0d9488",
|
|
114
|
+
property: "#db2777",
|
|
115
|
+
operator: "#64748b",
|
|
116
|
+
punctuation: "currentColor"
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/theme.ts
|
|
120
|
+
const defaultTheme = {
|
|
121
|
+
fontFamily: "sans-serif",
|
|
122
|
+
monoFamily: "monospace",
|
|
123
|
+
fontSize: 16,
|
|
124
|
+
lineHeight: 26,
|
|
125
|
+
color: "inherit",
|
|
126
|
+
blockGap: 16,
|
|
127
|
+
heading: {
|
|
128
|
+
weight: 650,
|
|
129
|
+
color: "inherit",
|
|
130
|
+
sizes: {
|
|
131
|
+
1: 32,
|
|
132
|
+
2: 26,
|
|
133
|
+
3: 21,
|
|
134
|
+
4: 18,
|
|
135
|
+
5: 16,
|
|
136
|
+
6: 15
|
|
137
|
+
},
|
|
138
|
+
lineHeights: {
|
|
139
|
+
1: 40,
|
|
140
|
+
2: 34,
|
|
141
|
+
3: 28,
|
|
142
|
+
4: 26,
|
|
143
|
+
5: 24,
|
|
144
|
+
6: 22
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
strongWeight: 700,
|
|
148
|
+
emphasisItalic: true,
|
|
149
|
+
link: {
|
|
150
|
+
color: "#2563eb",
|
|
151
|
+
underline: true
|
|
152
|
+
},
|
|
153
|
+
inlineCode: {
|
|
154
|
+
color: "inherit",
|
|
155
|
+
background: "rgba(127, 127, 127, 0.16)",
|
|
156
|
+
sizeScale: .9
|
|
157
|
+
},
|
|
158
|
+
code: {
|
|
159
|
+
fontSize: 13.5,
|
|
160
|
+
lineHeight: 21,
|
|
161
|
+
padding: 14,
|
|
162
|
+
background: "rgba(127, 127, 127, 0.12)",
|
|
163
|
+
color: "inherit",
|
|
164
|
+
radius: 8,
|
|
165
|
+
highlight: defaultTokenColors
|
|
166
|
+
},
|
|
167
|
+
blockquote: {
|
|
168
|
+
padding: 14,
|
|
169
|
+
gap: 12,
|
|
170
|
+
borderWidth: 3,
|
|
171
|
+
borderColor: "rgba(127, 127, 127, 0.4)",
|
|
172
|
+
color: "inherit"
|
|
173
|
+
},
|
|
174
|
+
list: {
|
|
175
|
+
gap: 6,
|
|
176
|
+
indent: 28,
|
|
177
|
+
markerColor: "inherit"
|
|
178
|
+
},
|
|
179
|
+
table: {
|
|
180
|
+
cellPadding: 8,
|
|
181
|
+
gap: 1,
|
|
182
|
+
headerWeight: 650,
|
|
183
|
+
headerBackground: "rgba(127, 127, 127, 0.12)",
|
|
184
|
+
borderColor: "rgba(127, 127, 127, 0.35)",
|
|
185
|
+
radius: 8
|
|
186
|
+
},
|
|
187
|
+
rule: {
|
|
188
|
+
thickness: 1,
|
|
189
|
+
color: "rgba(127, 127, 127, 0.4)",
|
|
190
|
+
gap: 8
|
|
191
|
+
},
|
|
192
|
+
image: {
|
|
193
|
+
placeholderHeight: 0,
|
|
194
|
+
color: "rgba(127, 127, 127, 0.7)"
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
function isObject(v) {
|
|
198
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
199
|
+
}
|
|
200
|
+
function deepMerge(base, patch) {
|
|
201
|
+
if (!isObject(patch)) return base;
|
|
202
|
+
const out = { ...base };
|
|
203
|
+
for (const key of Object.keys(patch)) {
|
|
204
|
+
const b = base[key];
|
|
205
|
+
const p = patch[key];
|
|
206
|
+
out[key] = isObject(b) && isObject(p) ? deepMerge(b, p) : p;
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
const resolveCache = /* @__PURE__ */ new WeakMap();
|
|
211
|
+
/** Merge a partial theme over the defaults into a fully-resolved theme. */
|
|
212
|
+
function resolveTheme(theme) {
|
|
213
|
+
if (theme == null) return defaultTheme;
|
|
214
|
+
const cached = resolveCache.get(theme);
|
|
215
|
+
if (cached !== void 0) return cached;
|
|
216
|
+
const resolved = deepMerge(defaultTheme, theme);
|
|
217
|
+
resolveCache.set(theme, resolved);
|
|
218
|
+
return resolved;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/inline.ts
|
|
222
|
+
/** A base format for body text at a given size/weight/colour. */
|
|
223
|
+
function baseFormat(theme, opts = {}) {
|
|
224
|
+
return {
|
|
225
|
+
family: theme.fontFamily,
|
|
226
|
+
monoFamily: theme.monoFamily,
|
|
227
|
+
size: opts.size ?? theme.fontSize,
|
|
228
|
+
weight: opts.weight ?? 400,
|
|
229
|
+
italic: false,
|
|
230
|
+
mono: false,
|
|
231
|
+
underline: false,
|
|
232
|
+
strike: false,
|
|
233
|
+
color: opts.color ?? (theme.color !== "inherit" ? theme.color : void 0)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Compose a measurable `Font` shorthand from a format. */
|
|
237
|
+
function composeFont(fmt) {
|
|
238
|
+
const family = fmt.mono ? fmt.monoFamily : fmt.family;
|
|
239
|
+
return `${fmt.italic ? "italic " : ""}${fmt.weight} ${fmt.size}px ${family}`;
|
|
240
|
+
}
|
|
241
|
+
function pushRun(out, text, fmt) {
|
|
242
|
+
if (text.length === 0) return;
|
|
243
|
+
const run = {
|
|
244
|
+
text,
|
|
245
|
+
font: composeFont(fmt)
|
|
246
|
+
};
|
|
247
|
+
if (fmt.color != null) run.color = fmt.color;
|
|
248
|
+
if (fmt.background != null) run.background = fmt.background;
|
|
249
|
+
const decoration = [fmt.underline ? "underline" : "", fmt.strike ? "line-through" : ""].filter(Boolean).join(" ");
|
|
250
|
+
if (decoration) run.decoration = decoration;
|
|
251
|
+
if (fmt.href != null) {
|
|
252
|
+
run.href = fmt.href;
|
|
253
|
+
run.as = "a";
|
|
254
|
+
} else if (fmt.mono) run.as = "code";
|
|
255
|
+
out.push(run);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Flatten phrasing content into styled runs. Recursive over the inline marks;
|
|
259
|
+
* the result feeds a single `<RichText>` so the whole paragraph wraps as one
|
|
260
|
+
* flow and measures exactly.
|
|
261
|
+
*/
|
|
262
|
+
function flattenInline(nodes, fmt, theme, out) {
|
|
263
|
+
for (const node of nodes) switch (node.type) {
|
|
264
|
+
case "text":
|
|
265
|
+
pushRun(out, node.value, fmt);
|
|
266
|
+
break;
|
|
267
|
+
case "strong":
|
|
268
|
+
flattenInline(node.children, {
|
|
269
|
+
...fmt,
|
|
270
|
+
weight: theme.strongWeight
|
|
271
|
+
}, theme, out);
|
|
272
|
+
break;
|
|
273
|
+
case "emphasis":
|
|
274
|
+
flattenInline(node.children, {
|
|
275
|
+
...fmt,
|
|
276
|
+
italic: theme.emphasisItalic ? true : fmt.italic
|
|
277
|
+
}, theme, out);
|
|
278
|
+
break;
|
|
279
|
+
case "delete":
|
|
280
|
+
flattenInline(node.children, {
|
|
281
|
+
...fmt,
|
|
282
|
+
strike: true
|
|
283
|
+
}, theme, out);
|
|
284
|
+
break;
|
|
285
|
+
case "inlineCode":
|
|
286
|
+
pushRun(out, node.value, {
|
|
287
|
+
...fmt,
|
|
288
|
+
mono: true,
|
|
289
|
+
size: Math.round(fmt.size * theme.inlineCode.sizeScale),
|
|
290
|
+
color: theme.inlineCode.color !== "inherit" ? theme.inlineCode.color : fmt.color,
|
|
291
|
+
background: theme.inlineCode.background
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
case "link":
|
|
295
|
+
flattenInline(node.children, {
|
|
296
|
+
...fmt,
|
|
297
|
+
href: node.url,
|
|
298
|
+
color: theme.link.color,
|
|
299
|
+
underline: theme.link.underline ? true : fmt.underline
|
|
300
|
+
}, theme, out);
|
|
301
|
+
break;
|
|
302
|
+
case "linkReference":
|
|
303
|
+
flattenInline(node.children, fmt, theme, out);
|
|
304
|
+
break;
|
|
305
|
+
case "break":
|
|
306
|
+
out.push({
|
|
307
|
+
text: "",
|
|
308
|
+
break: true
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
case "image":
|
|
312
|
+
if (node.alt) pushRun(out, node.alt, {
|
|
313
|
+
...fmt,
|
|
314
|
+
color: theme.image.color
|
|
315
|
+
});
|
|
316
|
+
break;
|
|
317
|
+
case "imageReference":
|
|
318
|
+
if (node.alt) pushRun(out, node.alt, {
|
|
319
|
+
...fmt,
|
|
320
|
+
color: theme.image.color
|
|
321
|
+
});
|
|
322
|
+
break;
|
|
323
|
+
case "footnoteReference":
|
|
324
|
+
pushRun(out, `[${node.label ?? node.identifier}]`, {
|
|
325
|
+
...fmt,
|
|
326
|
+
color: theme.link.color
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
if ("children" in node && Array.isArray(node.children)) flattenInline(node.children, fmt, theme, out);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/primitives/rich-text.tsx
|
|
336
|
+
function resolveRunFont(run, fallback) {
|
|
337
|
+
const font = run.font ?? fallback;
|
|
338
|
+
if (font == null) throw new Error("mugen-markdown: <RichText> run needs a font — set `font` on the run or on <RichText>.");
|
|
339
|
+
(0, _wingleeio_mugen.assertMeasurableFont)(font);
|
|
340
|
+
return font;
|
|
341
|
+
}
|
|
342
|
+
/** Split runs into hard-break-delimited segments, each a list of rich-inline items. */
|
|
343
|
+
function segmentItems(runs, fallback) {
|
|
344
|
+
const segments = [];
|
|
345
|
+
let cur = [];
|
|
346
|
+
for (const run of runs) {
|
|
347
|
+
if (run.break) {
|
|
348
|
+
segments.push(cur);
|
|
349
|
+
cur = [];
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (run.text.length === 0) continue;
|
|
353
|
+
const item = {
|
|
354
|
+
text: run.text,
|
|
355
|
+
font: resolveRunFont(run, fallback)
|
|
356
|
+
};
|
|
357
|
+
if (run.letterSpacing != null) item.letterSpacing = run.letterSpacing;
|
|
358
|
+
if (run.noBreak) item.break = "never";
|
|
359
|
+
cur.push(item);
|
|
360
|
+
}
|
|
361
|
+
segments.push(cur);
|
|
362
|
+
return segments;
|
|
363
|
+
}
|
|
364
|
+
const MAX_CACHE = 4096;
|
|
365
|
+
const prepCache = /* @__PURE__ */ new Map();
|
|
366
|
+
let cacheEpoch = -1;
|
|
367
|
+
function segmentKey(items) {
|
|
368
|
+
let key = "";
|
|
369
|
+
for (const it of items) key += `${it.font}${it.letterSpacing ?? ""}${it.break ?? ""}${it.text}`;
|
|
370
|
+
return key;
|
|
371
|
+
}
|
|
372
|
+
function prepareCached(items) {
|
|
373
|
+
const epoch = (0, _wingleeio_mugen.fontEpoch)();
|
|
374
|
+
if (epoch !== cacheEpoch) {
|
|
375
|
+
prepCache.clear();
|
|
376
|
+
cacheEpoch = epoch;
|
|
377
|
+
}
|
|
378
|
+
const key = segmentKey(items);
|
|
379
|
+
let prepared = prepCache.get(key);
|
|
380
|
+
if (prepared === void 0) {
|
|
381
|
+
if (prepCache.size >= MAX_CACHE) prepCache.clear();
|
|
382
|
+
prepared = (0, _chenglou_pretext_rich_inline.prepareRichInline)(items);
|
|
383
|
+
prepCache.set(key, prepared);
|
|
384
|
+
}
|
|
385
|
+
return prepared;
|
|
386
|
+
}
|
|
387
|
+
function measureRichText(props, ctx) {
|
|
388
|
+
const { runs, lineHeight } = props;
|
|
389
|
+
if (runs.length === 0) return 0;
|
|
390
|
+
const segments = segmentItems(runs, props.font);
|
|
391
|
+
const hasText = segments.some((s) => s.length > 0);
|
|
392
|
+
const hasBreak = runs.some((r) => r.break);
|
|
393
|
+
if (!hasText && !hasBreak) return 0;
|
|
394
|
+
const width = Math.max(0, ctx.width);
|
|
395
|
+
let lines = 0;
|
|
396
|
+
for (const seg of segments) {
|
|
397
|
+
if (seg.length === 0) {
|
|
398
|
+
lines += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
lines += Math.max(1, (0, _chenglou_pretext_rich_inline.measureRichInlineStats)(prepareCached(seg), width).lineCount);
|
|
402
|
+
}
|
|
403
|
+
return lines * lineHeight;
|
|
404
|
+
}
|
|
405
|
+
function renderRichText(props) {
|
|
406
|
+
const lh = props.lineHeight;
|
|
407
|
+
const containerStyle = {
|
|
408
|
+
...props.font != null ? (0, _wingleeio_mugen.fontLonghands)(props.font, lh) : { lineHeight: `${lh}px` },
|
|
409
|
+
whiteSpace: "normal",
|
|
410
|
+
overflowWrap: "anywhere",
|
|
411
|
+
margin: 0,
|
|
412
|
+
padding: 0,
|
|
413
|
+
...props.color != null ? { color: props.color } : null,
|
|
414
|
+
...props.align != null ? { textAlign: props.align } : null,
|
|
415
|
+
...props.style
|
|
416
|
+
};
|
|
417
|
+
const children = props.runs.map((run, i) => {
|
|
418
|
+
if (run.break) return (0, react.createElement)("br", { key: i });
|
|
419
|
+
const tag = run.as ?? (run.href != null ? "a" : "span");
|
|
420
|
+
const elementProps = {
|
|
421
|
+
key: i,
|
|
422
|
+
style: {
|
|
423
|
+
...(0, _wingleeio_mugen.fontLonghands)(resolveRunFont(run, props.font), 0),
|
|
424
|
+
fontVariantLigatures: "normal",
|
|
425
|
+
fontFeatureSettings: "normal",
|
|
426
|
+
letterSpacing: run.letterSpacing != null ? `${run.letterSpacing}px` : "normal",
|
|
427
|
+
...run.color != null ? { color: run.color } : null,
|
|
428
|
+
...run.background != null ? { background: run.background } : null,
|
|
429
|
+
...run.decoration != null ? { textDecoration: run.decoration } : null,
|
|
430
|
+
...run.noBreak ? { whiteSpace: "nowrap" } : null
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
if (run.href != null) elementProps.href = run.href;
|
|
434
|
+
if (run.title != null) elementProps.title = run.title;
|
|
435
|
+
if (run.onClick != null) elementProps.onClick = run.onClick;
|
|
436
|
+
if (run.className != null) elementProps.className = run.className;
|
|
437
|
+
return (0, react.createElement)(tag, elementProps, run.text);
|
|
438
|
+
});
|
|
439
|
+
return (0, react.createElement)("div", {
|
|
440
|
+
className: props.className,
|
|
441
|
+
style: containerStyle
|
|
442
|
+
}, children);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* A measurable rich inline-text primitive: a paragraph of mixed-font runs that
|
|
446
|
+
* wrap as one flow. Its height is `lines × lineHeight`, where `lines` comes from
|
|
447
|
+
* pretext's rich-inline layout at the row's width — the same layout the browser
|
|
448
|
+
* performs over the rendered spans, so the analytic height matches the paint.
|
|
449
|
+
*/
|
|
450
|
+
const RichText = (0, _wingleeio_mugen.markPrimitive)(renderRichText, {
|
|
451
|
+
name: "RichText",
|
|
452
|
+
measure: (props, ctx) => measureRichText(props, ctx),
|
|
453
|
+
naturalWidth: (props) => {
|
|
454
|
+
const p = props;
|
|
455
|
+
if (p.runs.length === 0) return 0;
|
|
456
|
+
const segments = segmentItems(p.runs, p.font);
|
|
457
|
+
let max = 0;
|
|
458
|
+
for (const seg of segments) {
|
|
459
|
+
if (seg.length === 0) continue;
|
|
460
|
+
max = Math.max(max, (0, _chenglou_pretext_rich_inline.measureRichInlineStats)(prepareCached(seg), 1e7).maxLineWidth);
|
|
461
|
+
}
|
|
462
|
+
return max;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
/** Drop the rich-inline prepare cache (tests / memory pressure). */
|
|
466
|
+
function clearRichTextCache() {
|
|
467
|
+
prepCache.clear();
|
|
468
|
+
cacheEpoch = -1;
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/highlight/languages.ts
|
|
472
|
+
function words(s) {
|
|
473
|
+
return new Set(s.split(" "));
|
|
474
|
+
}
|
|
475
|
+
const NONE = /* @__PURE__ */ new Set();
|
|
476
|
+
function profile(p) {
|
|
477
|
+
return {
|
|
478
|
+
lineComments: p.lineComments ?? [],
|
|
479
|
+
blockComments: p.blockComments ?? [],
|
|
480
|
+
quotes: p.quotes ?? ["\"", "'"],
|
|
481
|
+
multilineQuotes: p.multilineQuotes ?? [],
|
|
482
|
+
keywords: p.keywords ?? NONE,
|
|
483
|
+
constants: p.constants ?? NONE,
|
|
484
|
+
capitalTypes: p.capitalTypes ?? false,
|
|
485
|
+
identExtra: p.identExtra ?? "",
|
|
486
|
+
...p.caseInsensitive ? { caseInsensitive: true } : null,
|
|
487
|
+
...p.stringKeys ? { stringKeys: true } : null,
|
|
488
|
+
...p.colonProps ? { colonProps: true } : null,
|
|
489
|
+
...p.eqProps ? { eqProps: true } : null,
|
|
490
|
+
...p.tags ? { tags: true } : null
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const jsTs = profile({
|
|
494
|
+
lineComments: ["//"],
|
|
495
|
+
blockComments: [["/*", "*/"]],
|
|
496
|
+
multilineQuotes: ["`"],
|
|
497
|
+
keywords: words("abstract any as asserts async await bigint boolean break case catch class const continue debugger declare default delete do else enum export extends finally for from function get if implements import in infer instanceof interface is keyof let namespace never new number object of out override package private protected public readonly return satisfies set static string super switch symbol this throw try type typeof unique unknown var void while with yield"),
|
|
498
|
+
constants: words("true false null undefined NaN Infinity globalThis"),
|
|
499
|
+
capitalTypes: true,
|
|
500
|
+
colonProps: true
|
|
501
|
+
});
|
|
502
|
+
const python = profile({
|
|
503
|
+
lineComments: ["#"],
|
|
504
|
+
multilineQuotes: ["\"\"\"", "'''"],
|
|
505
|
+
keywords: words("and as assert async await break class continue def del elif else except finally for from global if import in is lambda match nonlocal not or pass raise return try while with yield"),
|
|
506
|
+
constants: words("True False None self cls"),
|
|
507
|
+
capitalTypes: true
|
|
508
|
+
});
|
|
509
|
+
const rust = profile({
|
|
510
|
+
lineComments: ["//"],
|
|
511
|
+
blockComments: [["/*", "*/"]],
|
|
512
|
+
quotes: ["\""],
|
|
513
|
+
keywords: words("as async await break const continue crate dyn else enum extern fn for if impl in let loop macro match mod move mut pub ref return self Self static struct super trait type union unsafe use where while"),
|
|
514
|
+
constants: words("true false"),
|
|
515
|
+
capitalTypes: true
|
|
516
|
+
});
|
|
517
|
+
const go = profile({
|
|
518
|
+
lineComments: ["//"],
|
|
519
|
+
blockComments: [["/*", "*/"]],
|
|
520
|
+
multilineQuotes: ["`"],
|
|
521
|
+
keywords: words("any break case chan const continue default defer else error fallthrough for func go goto if import interface map package range return select string struct switch type var int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float32 float64 byte rune bool"),
|
|
522
|
+
constants: words("true false nil iota")
|
|
523
|
+
});
|
|
524
|
+
const cLike = (extra) => profile({
|
|
525
|
+
lineComments: ["//"],
|
|
526
|
+
blockComments: [["/*", "*/"]],
|
|
527
|
+
keywords: words("auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while " + extra),
|
|
528
|
+
constants: words("true false NULL nullptr"),
|
|
529
|
+
capitalTypes: true
|
|
530
|
+
});
|
|
531
|
+
const c = cLike("");
|
|
532
|
+
const cpp = cLike("alignas alignof bool catch class concept constexpr consteval constinit decltype delete dynamic_cast explicit export friend mutable namespace new noexcept operator private protected public reinterpret_cast requires static_assert static_cast template this thread_local throw try typeid typename using virtual wchar_t co_await co_return co_yield");
|
|
533
|
+
const java = profile({
|
|
534
|
+
lineComments: ["//"],
|
|
535
|
+
blockComments: [["/*", "*/"]],
|
|
536
|
+
keywords: words("abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import instanceof int interface long native new package permits private protected public record return sealed short static strictfp super switch synchronized this throw throws transient try var void volatile while yield"),
|
|
537
|
+
constants: words("true false null"),
|
|
538
|
+
capitalTypes: true
|
|
539
|
+
});
|
|
540
|
+
const csharp = profile({
|
|
541
|
+
lineComments: ["//"],
|
|
542
|
+
blockComments: [["/*", "*/"]],
|
|
543
|
+
keywords: words("abstract as async await base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new object operator out override params private protected public readonly record ref return sbyte sealed short sizeof stackalloc static string struct switch this throw try typeof uint ulong unchecked unsafe ushort using var virtual void volatile when where while yield"),
|
|
544
|
+
constants: words("true false null"),
|
|
545
|
+
capitalTypes: true
|
|
546
|
+
});
|
|
547
|
+
const php = profile({
|
|
548
|
+
lineComments: ["//", "#"],
|
|
549
|
+
blockComments: [["/*", "*/"]],
|
|
550
|
+
keywords: words("abstract and array as break callable case catch class clone const continue declare default do echo else elseif empty enum extends final finally fn for foreach function global goto if implements include include_once instanceof insteadof interface isset list match namespace new or print private protected public readonly require require_once return static switch throw trait try unset use var while xor yield"),
|
|
551
|
+
constants: words("true false null TRUE FALSE NULL"),
|
|
552
|
+
capitalTypes: true
|
|
553
|
+
});
|
|
554
|
+
const ruby = profile({
|
|
555
|
+
lineComments: ["#"],
|
|
556
|
+
keywords: words("alias and begin break case class def defined? do else elsif end ensure for if in module next not or redo rescue retry return super then undef unless until when while yield require require_relative attr_accessor attr_reader attr_writer"),
|
|
557
|
+
constants: words("true false nil self"),
|
|
558
|
+
capitalTypes: true
|
|
559
|
+
});
|
|
560
|
+
const swift = profile({
|
|
561
|
+
lineComments: ["//"],
|
|
562
|
+
blockComments: [["/*", "*/"]],
|
|
563
|
+
multilineQuotes: ["\"\"\""],
|
|
564
|
+
keywords: words("as associatedtype await break case catch class continue convenience default defer deinit didSet do dynamic else enum extension fallthrough fileprivate final for func get guard if import in indirect infix init inout internal is lazy let mutating nonmutating open operator optional override postfix prefix private protocol public repeat required rethrows return set some static struct subscript super switch throw throws try typealias unowned var weak where while willSet"),
|
|
565
|
+
constants: words("true false nil"),
|
|
566
|
+
capitalTypes: true
|
|
567
|
+
});
|
|
568
|
+
const kotlin = profile({
|
|
569
|
+
lineComments: ["//"],
|
|
570
|
+
blockComments: [["/*", "*/"]],
|
|
571
|
+
multilineQuotes: ["\"\"\""],
|
|
572
|
+
keywords: words("abstract actual annotation as break by catch class companion const constructor continue crossinline data do else enum expect external final finally for fun get if import in infix init inline inner interface internal is lateinit noinline object open operator out override package private protected public reified return sealed set super suspend tailrec this throw try typealias val var vararg when where while"),
|
|
573
|
+
constants: words("true false null"),
|
|
574
|
+
capitalTypes: true
|
|
575
|
+
});
|
|
576
|
+
const shell = profile({
|
|
577
|
+
lineComments: ["#"],
|
|
578
|
+
keywords: words("if then else elif fi for while until do done case esac function in select time return exit export local readonly declare unset shift break continue eval exec set source alias trap")
|
|
579
|
+
});
|
|
580
|
+
const sql = profile({
|
|
581
|
+
lineComments: ["--"],
|
|
582
|
+
blockComments: [["/*", "*/"]],
|
|
583
|
+
quotes: [
|
|
584
|
+
"\"",
|
|
585
|
+
"'",
|
|
586
|
+
"`"
|
|
587
|
+
],
|
|
588
|
+
caseInsensitive: true,
|
|
589
|
+
keywords: words("select from where insert into values update delete set create table alter drop index view as join inner left right full outer on group by order having limit offset union all distinct and or not is in like between exists case when then else end primary key foreign references default constraint unique check if begin commit rollback transaction with returning cascade"),
|
|
590
|
+
constants: words("null true false")
|
|
591
|
+
});
|
|
592
|
+
const css = profile({
|
|
593
|
+
blockComments: [["/*", "*/"]],
|
|
594
|
+
identExtra: "-@#",
|
|
595
|
+
keywords: words("@media @import @charset @namespace @supports @document @page @font-face @keyframes @counter-style @font-feature-values @layer @container @property @scope"),
|
|
596
|
+
constants: words("inherit initial unset revert auto none important"),
|
|
597
|
+
colonProps: true
|
|
598
|
+
});
|
|
599
|
+
const scss = profile({
|
|
600
|
+
lineComments: ["//"],
|
|
601
|
+
blockComments: [["/*", "*/"]],
|
|
602
|
+
identExtra: "-@#$",
|
|
603
|
+
keywords: words("@media @import @use @forward @mixin @include @function @return @if @else @each @for @while @extend @keyframes @supports @layer @container @font-face @charset @debug @warn @error"),
|
|
604
|
+
constants: words("inherit initial unset revert auto none important"),
|
|
605
|
+
colonProps: true
|
|
606
|
+
});
|
|
607
|
+
const html = profile({
|
|
608
|
+
blockComments: [["<!--", "-->"]],
|
|
609
|
+
identExtra: "-",
|
|
610
|
+
tags: true,
|
|
611
|
+
eqProps: true
|
|
612
|
+
});
|
|
613
|
+
const json = profile({
|
|
614
|
+
lineComments: ["//"],
|
|
615
|
+
blockComments: [["/*", "*/"]],
|
|
616
|
+
stringKeys: true,
|
|
617
|
+
constants: words("true false null")
|
|
618
|
+
});
|
|
619
|
+
const yaml = profile({
|
|
620
|
+
lineComments: ["#"],
|
|
621
|
+
colonProps: true,
|
|
622
|
+
constants: words("true false null yes no on off True False Null Yes No On Off")
|
|
623
|
+
});
|
|
624
|
+
const toml = profile({
|
|
625
|
+
lineComments: ["#"],
|
|
626
|
+
multilineQuotes: ["\"\"\"", "'''"],
|
|
627
|
+
eqProps: true,
|
|
628
|
+
constants: words("true false")
|
|
629
|
+
});
|
|
630
|
+
const ini = profile({
|
|
631
|
+
lineComments: ["#", ";"],
|
|
632
|
+
eqProps: true
|
|
633
|
+
});
|
|
634
|
+
const dockerfile = profile({
|
|
635
|
+
lineComments: ["#"],
|
|
636
|
+
caseInsensitive: true,
|
|
637
|
+
keywords: words("from run cmd copy add env arg workdir expose entrypoint volume user label onbuild stopsignal healthcheck shell as")
|
|
638
|
+
});
|
|
639
|
+
const registry = /* @__PURE__ */ new Map();
|
|
640
|
+
/**
|
|
641
|
+
* Register a profile under one or more fence info-string names (lower-cased).
|
|
642
|
+
* Use this to add or override languages for the built-in highlighter.
|
|
643
|
+
*/
|
|
644
|
+
function registerLanguage(names, p) {
|
|
645
|
+
for (const name of typeof names === "string" ? [names] : names) registry.set(name.toLowerCase(), p);
|
|
646
|
+
}
|
|
647
|
+
registerLanguage([
|
|
648
|
+
"javascript",
|
|
649
|
+
"js",
|
|
650
|
+
"jsx",
|
|
651
|
+
"mjs",
|
|
652
|
+
"cjs"
|
|
653
|
+
], jsTs);
|
|
654
|
+
registerLanguage([
|
|
655
|
+
"typescript",
|
|
656
|
+
"ts",
|
|
657
|
+
"tsx",
|
|
658
|
+
"mts",
|
|
659
|
+
"cts"
|
|
660
|
+
], jsTs);
|
|
661
|
+
registerLanguage([
|
|
662
|
+
"python",
|
|
663
|
+
"py",
|
|
664
|
+
"python3"
|
|
665
|
+
], python);
|
|
666
|
+
registerLanguage(["rust", "rs"], rust);
|
|
667
|
+
registerLanguage(["go", "golang"], go);
|
|
668
|
+
registerLanguage(["java"], java);
|
|
669
|
+
registerLanguage(["c", "h"], c);
|
|
670
|
+
registerLanguage([
|
|
671
|
+
"cpp",
|
|
672
|
+
"c++",
|
|
673
|
+
"cc",
|
|
674
|
+
"cxx",
|
|
675
|
+
"hpp",
|
|
676
|
+
"hxx",
|
|
677
|
+
"objc",
|
|
678
|
+
"objective-c"
|
|
679
|
+
], cpp);
|
|
680
|
+
registerLanguage([
|
|
681
|
+
"csharp",
|
|
682
|
+
"cs",
|
|
683
|
+
"c#"
|
|
684
|
+
], csharp);
|
|
685
|
+
registerLanguage(["php"], php);
|
|
686
|
+
registerLanguage(["ruby", "rb"], ruby);
|
|
687
|
+
registerLanguage(["swift"], swift);
|
|
688
|
+
registerLanguage([
|
|
689
|
+
"kotlin",
|
|
690
|
+
"kt",
|
|
691
|
+
"kts"
|
|
692
|
+
], kotlin);
|
|
693
|
+
registerLanguage([
|
|
694
|
+
"shell",
|
|
695
|
+
"bash",
|
|
696
|
+
"sh",
|
|
697
|
+
"zsh",
|
|
698
|
+
"fish",
|
|
699
|
+
"console",
|
|
700
|
+
"shellsession"
|
|
701
|
+
], shell);
|
|
702
|
+
registerLanguage([
|
|
703
|
+
"sql",
|
|
704
|
+
"mysql",
|
|
705
|
+
"postgres",
|
|
706
|
+
"postgresql",
|
|
707
|
+
"sqlite",
|
|
708
|
+
"plsql"
|
|
709
|
+
], sql);
|
|
710
|
+
registerLanguage(["css"], css);
|
|
711
|
+
registerLanguage([
|
|
712
|
+
"scss",
|
|
713
|
+
"sass",
|
|
714
|
+
"less"
|
|
715
|
+
], scss);
|
|
716
|
+
registerLanguage([
|
|
717
|
+
"html",
|
|
718
|
+
"xml",
|
|
719
|
+
"svg",
|
|
720
|
+
"xhtml",
|
|
721
|
+
"markup",
|
|
722
|
+
"vue",
|
|
723
|
+
"astro"
|
|
724
|
+
], html);
|
|
725
|
+
registerLanguage([
|
|
726
|
+
"json",
|
|
727
|
+
"jsonc",
|
|
728
|
+
"json5"
|
|
729
|
+
], json);
|
|
730
|
+
registerLanguage(["yaml", "yml"], yaml);
|
|
731
|
+
registerLanguage(["toml"], toml);
|
|
732
|
+
registerLanguage(["ini"], ini);
|
|
733
|
+
registerLanguage(["dockerfile", "docker"], dockerfile);
|
|
734
|
+
/** The profile for a fence language, or `null` when the language is unknown. */
|
|
735
|
+
function profileFor(lang) {
|
|
736
|
+
if (lang == null || lang.length === 0) return null;
|
|
737
|
+
return registry.get(lang.trim().toLowerCase()) ?? null;
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/highlight/tokenize.ts
|
|
741
|
+
const INITIAL_STATE = {
|
|
742
|
+
mode: "plain",
|
|
743
|
+
close: ""
|
|
744
|
+
};
|
|
745
|
+
const OPERATORS = "+-*/%=<>!&|^~?";
|
|
746
|
+
const PUNCTUATION = "()[]{};,.:";
|
|
747
|
+
function isDigit(c) {
|
|
748
|
+
return c >= "0" && c <= "9";
|
|
749
|
+
}
|
|
750
|
+
function isIdentStart(c, extra) {
|
|
751
|
+
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$" || c > "" || extra.length > 0 && extra.includes(c);
|
|
752
|
+
}
|
|
753
|
+
function isIdentPart(c, extra) {
|
|
754
|
+
return isIdentStart(c, extra) || isDigit(c);
|
|
755
|
+
}
|
|
756
|
+
/** Scan a string body from `from` to its closing delimiter (or end-of-line). */
|
|
757
|
+
function scanStringBody(line, from, close) {
|
|
758
|
+
const n = line.length;
|
|
759
|
+
let j = from;
|
|
760
|
+
while (j < n) {
|
|
761
|
+
if (line.charCodeAt(j) === 92) {
|
|
762
|
+
j += 2;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (line.startsWith(close, j)) return {
|
|
766
|
+
end: j + close.length,
|
|
767
|
+
closed: true
|
|
768
|
+
};
|
|
769
|
+
j++;
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
end: n,
|
|
773
|
+
closed: false
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Loose number scan: digits, radix prefixes, separators, exponents, suffixes. */
|
|
777
|
+
function scanNumber(line, start) {
|
|
778
|
+
const n = line.length;
|
|
779
|
+
let j = start;
|
|
780
|
+
while (j < n) {
|
|
781
|
+
const c = line[j];
|
|
782
|
+
if (isDigit(c) || c === "." || c === "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z") {
|
|
783
|
+
j++;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const prev = line[j - 1];
|
|
787
|
+
if ((c === "+" || c === "-") && (prev === "e" || prev === "E" || prev === "p" || prev === "P")) {
|
|
788
|
+
j++;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
return j;
|
|
794
|
+
}
|
|
795
|
+
function nextNonSpaceIdx(line, j) {
|
|
796
|
+
const n = line.length;
|
|
797
|
+
while (j < n) {
|
|
798
|
+
const c = line[j];
|
|
799
|
+
if (c !== " " && c !== " ") return j;
|
|
800
|
+
j++;
|
|
801
|
+
}
|
|
802
|
+
return -1;
|
|
803
|
+
}
|
|
804
|
+
function classifyWord(line, start, end, prev, p) {
|
|
805
|
+
const word = line.slice(start, end);
|
|
806
|
+
const key = p.caseInsensitive === true ? word.toLowerCase() : word;
|
|
807
|
+
if (p.keywords.has(key)) return "keyword";
|
|
808
|
+
if (p.constants.has(key)) return "constant";
|
|
809
|
+
if (p.tags === true && (prev === "<" || prev === "/")) return "keyword";
|
|
810
|
+
const ni = nextNonSpaceIdx(line, end);
|
|
811
|
+
const next = ni < 0 ? "" : line[ni];
|
|
812
|
+
if (next === "(") return "function";
|
|
813
|
+
if (p.eqProps === true && next === "=" && line[ni + 1] !== "=") return "property";
|
|
814
|
+
if (p.colonProps === true && line[end] === ":" && line[end + 1] !== ":") return "property";
|
|
815
|
+
if (prev === ".") return "property";
|
|
816
|
+
if (p.capitalTypes) {
|
|
817
|
+
const c0 = line[start];
|
|
818
|
+
if (c0 >= "A" && c0 <= "Z") return "type";
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Tokenize one line given the state the previous line ended in. Returns the
|
|
824
|
+
* coloured tokens (sorted, non-overlapping) and the state this line ends in.
|
|
825
|
+
*/
|
|
826
|
+
function tokenizeLine(line, state, p) {
|
|
827
|
+
const tokens = [];
|
|
828
|
+
const n = line.length;
|
|
829
|
+
let i = 0;
|
|
830
|
+
const push = (start, end, type) => {
|
|
831
|
+
if (end > start) tokens.push({
|
|
832
|
+
start,
|
|
833
|
+
end,
|
|
834
|
+
type
|
|
835
|
+
});
|
|
836
|
+
};
|
|
837
|
+
if (state.mode === "comment") {
|
|
838
|
+
const j = line.indexOf(state.close);
|
|
839
|
+
if (j < 0) {
|
|
840
|
+
push(0, n, "comment");
|
|
841
|
+
return {
|
|
842
|
+
tokens,
|
|
843
|
+
end: state
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
i = j + state.close.length;
|
|
847
|
+
push(0, i, "comment");
|
|
848
|
+
} else if (state.mode === "string") {
|
|
849
|
+
const r = scanStringBody(line, 0, state.close);
|
|
850
|
+
push(0, r.end, "string");
|
|
851
|
+
if (!r.closed) return {
|
|
852
|
+
tokens,
|
|
853
|
+
end: state
|
|
854
|
+
};
|
|
855
|
+
i = r.end;
|
|
856
|
+
}
|
|
857
|
+
let prev = "";
|
|
858
|
+
outer: while (i < n) {
|
|
859
|
+
const ch = line[i];
|
|
860
|
+
if (ch === " " || ch === " ") {
|
|
861
|
+
i++;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
for (const lc of p.lineComments) if (line.startsWith(lc, i)) {
|
|
865
|
+
push(i, n, "comment");
|
|
866
|
+
i = n;
|
|
867
|
+
break outer;
|
|
868
|
+
}
|
|
869
|
+
for (const bc of p.blockComments) if (line.startsWith(bc[0], i)) {
|
|
870
|
+
const j = line.indexOf(bc[1], i + bc[0].length);
|
|
871
|
+
if (j < 0) {
|
|
872
|
+
push(i, n, "comment");
|
|
873
|
+
return {
|
|
874
|
+
tokens,
|
|
875
|
+
end: {
|
|
876
|
+
mode: "comment",
|
|
877
|
+
close: bc[1]
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
push(i, j + bc[1].length, "comment");
|
|
882
|
+
i = j + bc[1].length;
|
|
883
|
+
prev = "";
|
|
884
|
+
continue outer;
|
|
885
|
+
}
|
|
886
|
+
for (const q of p.multilineQuotes) if (line.startsWith(q, i)) {
|
|
887
|
+
const r = scanStringBody(line, i + q.length, q);
|
|
888
|
+
push(i, r.end, "string");
|
|
889
|
+
if (!r.closed) return {
|
|
890
|
+
tokens,
|
|
891
|
+
end: {
|
|
892
|
+
mode: "string",
|
|
893
|
+
close: q
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
i = r.end;
|
|
897
|
+
prev = q[0];
|
|
898
|
+
continue outer;
|
|
899
|
+
}
|
|
900
|
+
if (p.quotes.includes(ch)) {
|
|
901
|
+
const r = scanStringBody(line, i + 1, ch);
|
|
902
|
+
push(i, r.end, p.stringKeys === true && line[r.end] === ":" ? "property" : "string");
|
|
903
|
+
i = r.end;
|
|
904
|
+
prev = ch;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (isDigit(ch) || ch === "." && i + 1 < n && isDigit(line[i + 1])) {
|
|
908
|
+
const j = scanNumber(line, i);
|
|
909
|
+
push(i, j, "number");
|
|
910
|
+
i = j;
|
|
911
|
+
prev = "0";
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (isIdentStart(ch, p.identExtra)) {
|
|
915
|
+
let j = i + 1;
|
|
916
|
+
while (j < n && isIdentPart(line[j], p.identExtra)) j++;
|
|
917
|
+
const type = classifyWord(line, i, j, prev, p);
|
|
918
|
+
if (type != null) push(i, j, type);
|
|
919
|
+
i = j;
|
|
920
|
+
prev = "a";
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (OPERATORS.includes(ch)) {
|
|
924
|
+
let j = i + 1;
|
|
925
|
+
while (j < n && OPERATORS.includes(line[j])) j++;
|
|
926
|
+
push(i, j, "operator");
|
|
927
|
+
prev = line[j - 1];
|
|
928
|
+
i = j;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (PUNCTUATION.includes(ch)) {
|
|
932
|
+
push(i, i + 1, "punctuation");
|
|
933
|
+
prev = ch;
|
|
934
|
+
i++;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
prev = ch;
|
|
938
|
+
i++;
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
tokens,
|
|
942
|
+
end: INITIAL_STATE
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const TILE_LINES = 96;
|
|
946
|
+
const SYNC_BUDGET_MS = 4;
|
|
947
|
+
const CHUNK_BUDGET_MS = 6;
|
|
948
|
+
/** Dirty-line count beyond which we show the DOM text while re-tokenizing. */
|
|
949
|
+
const REVEAL_THRESHOLD = 512;
|
|
950
|
+
let sharedMeasure;
|
|
951
|
+
function measureCtx() {
|
|
952
|
+
if (sharedMeasure === void 0) {
|
|
953
|
+
sharedMeasure = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d", { willReadFrequently: false });
|
|
954
|
+
if (sharedMeasure != null && typeof sharedMeasure.measureText !== "function") sharedMeasure = null;
|
|
955
|
+
}
|
|
956
|
+
return sharedMeasure;
|
|
957
|
+
}
|
|
958
|
+
/** Visible lines, matching the measure's `lineCount` (trailing `\n` is silent). */
|
|
959
|
+
function splitLines(value) {
|
|
960
|
+
const parts = value.split("\n");
|
|
961
|
+
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
962
|
+
return parts;
|
|
963
|
+
}
|
|
964
|
+
var HighlightSession = class {
|
|
965
|
+
input = null;
|
|
966
|
+
lines = [];
|
|
967
|
+
tokens = [];
|
|
968
|
+
ends = [];
|
|
969
|
+
widths = [];
|
|
970
|
+
/** Number of lines tokenized so far (a prefix of `lines`). */
|
|
971
|
+
done = 0;
|
|
972
|
+
tiles = [];
|
|
973
|
+
/** Scrollable ancestors of the block (visibility window + scroll signals). */
|
|
974
|
+
scrollers = [];
|
|
975
|
+
/** True while the DOM text is visible and the overlay hidden. */
|
|
976
|
+
revealed = true;
|
|
977
|
+
plainColor = null;
|
|
978
|
+
metrics = null;
|
|
979
|
+
pending = null;
|
|
980
|
+
io = null;
|
|
981
|
+
unlisten = [];
|
|
982
|
+
update(next) {
|
|
983
|
+
if (measureCtx() == null) return;
|
|
984
|
+
const prev = this.input;
|
|
985
|
+
this.input = next;
|
|
986
|
+
if (prev == null) this.listen();
|
|
987
|
+
const fontChanged = prev == null || prev.font !== next.font || prev.lineHeight !== next.lineHeight;
|
|
988
|
+
const profileChanged = prev == null || prev.profile !== next.profile;
|
|
989
|
+
const colorsChanged = prev == null || prev.colors !== next.colors;
|
|
990
|
+
const lines = splitLines(next.value);
|
|
991
|
+
let dirty = 0;
|
|
992
|
+
if (!profileChanged) {
|
|
993
|
+
const max = Math.min(this.lines.length, lines.length);
|
|
994
|
+
while (dirty < max && this.lines[dirty] === lines[dirty]) dirty++;
|
|
995
|
+
if (dirty === max && this.lines.length === lines.length) dirty = lines.length;
|
|
996
|
+
}
|
|
997
|
+
this.lines = lines;
|
|
998
|
+
if (this.done > dirty) this.done = dirty;
|
|
999
|
+
this.widths.length = Math.min(this.widths.length, dirty);
|
|
1000
|
+
if (fontChanged) {
|
|
1001
|
+
this.metrics = null;
|
|
1002
|
+
this.widths.length = 0;
|
|
1003
|
+
}
|
|
1004
|
+
this.layoutTiles();
|
|
1005
|
+
const invalidateFrom = fontChanged || colorsChanged ? 0 : dirty;
|
|
1006
|
+
for (const t of this.tiles) t.dirtyFrom = Math.min(t.dirtyFrom, Math.max(invalidateFrom, t.start));
|
|
1007
|
+
if (colorsChanged) this.plainColor = null;
|
|
1008
|
+
if (!this.revealed && this.lines.length - this.done > REVEAL_THRESHOLD) this.reveal();
|
|
1009
|
+
if (this.lines.length === 0) this.reveal();
|
|
1010
|
+
if (this.pending != null) {
|
|
1011
|
+
clearTimeout(this.pending);
|
|
1012
|
+
this.pending = null;
|
|
1013
|
+
}
|
|
1014
|
+
this.work(SYNC_BUDGET_MS);
|
|
1015
|
+
}
|
|
1016
|
+
destroy() {
|
|
1017
|
+
if (this.pending != null) clearTimeout(this.pending);
|
|
1018
|
+
this.pending = null;
|
|
1019
|
+
this.io?.disconnect();
|
|
1020
|
+
this.io = null;
|
|
1021
|
+
for (const u of this.unlisten) u();
|
|
1022
|
+
this.unlisten = [];
|
|
1023
|
+
if (this.input != null) this.reveal();
|
|
1024
|
+
for (const t of this.tiles) t.canvas.remove();
|
|
1025
|
+
this.tiles = [];
|
|
1026
|
+
this.scrollers = [];
|
|
1027
|
+
this.input = null;
|
|
1028
|
+
}
|
|
1029
|
+
work(budget) {
|
|
1030
|
+
const { profile } = this.input;
|
|
1031
|
+
const t0 = performance.now();
|
|
1032
|
+
while (this.done < this.lines.length) {
|
|
1033
|
+
const i = this.done;
|
|
1034
|
+
const st = i === 0 ? INITIAL_STATE : this.ends[i - 1];
|
|
1035
|
+
const r = tokenizeLine(this.lines[i], st, profile);
|
|
1036
|
+
this.tokens[i] = r.tokens;
|
|
1037
|
+
this.ends[i] = r.end;
|
|
1038
|
+
this.done++;
|
|
1039
|
+
if ((this.done & 31) === 0 && performance.now() - t0 > budget) break;
|
|
1040
|
+
}
|
|
1041
|
+
this.syncVisibility();
|
|
1042
|
+
if (this.done < this.lines.length) this.pending = setTimeout(() => {
|
|
1043
|
+
this.pending = null;
|
|
1044
|
+
this.work(CHUNK_BUDGET_MS);
|
|
1045
|
+
}, 0);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Recompute which tiles are near the visible window, paint the dirty ones
|
|
1049
|
+
* (tokens permitting), free the far ones, and conceal/reveal accordingly.
|
|
1050
|
+
* Runs synchronously on scroll/resize, so a tile entering the window is
|
|
1051
|
+
* painted before the browser paints that frame — never a blank flash.
|
|
1052
|
+
*/
|
|
1053
|
+
syncVisibility() {
|
|
1054
|
+
const input = this.input;
|
|
1055
|
+
if (input == null) return;
|
|
1056
|
+
if (this.tiles.length > 0) {
|
|
1057
|
+
const codeTop = input.codeEl.getBoundingClientRect().top;
|
|
1058
|
+
let top = 0;
|
|
1059
|
+
let bottom = window.innerHeight;
|
|
1060
|
+
for (const sc of this.scrollers) {
|
|
1061
|
+
const r = sc.getBoundingClientRect();
|
|
1062
|
+
if (r.top > top) top = r.top;
|
|
1063
|
+
if (r.bottom < bottom) bottom = r.bottom;
|
|
1064
|
+
}
|
|
1065
|
+
const margin = Math.max(400, bottom - top);
|
|
1066
|
+
const { lineHeight } = input;
|
|
1067
|
+
for (const t of this.tiles) {
|
|
1068
|
+
const tileTop = codeTop + t.start * lineHeight;
|
|
1069
|
+
const tileBottom = codeTop + this.tileEnd(t) * lineHeight;
|
|
1070
|
+
if (tileBottom > top - margin && tileTop < bottom + margin) {
|
|
1071
|
+
t.visible = true;
|
|
1072
|
+
const end = this.tileEnd(t);
|
|
1073
|
+
if (t.dirtyFrom < end && this.done >= end) this.paintTile(t);
|
|
1074
|
+
} else {
|
|
1075
|
+
t.visible = false;
|
|
1076
|
+
if ((tileBottom < top - 2 * margin || tileTop > bottom + 2 * margin) && t.canvas.width !== 0) {
|
|
1077
|
+
t.canvas.width = 0;
|
|
1078
|
+
t.canvas.height = 0;
|
|
1079
|
+
t.cssWidth = 0;
|
|
1080
|
+
t.dirtyFrom = t.start;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
this.maybeConceal();
|
|
1086
|
+
}
|
|
1087
|
+
/** Swap DOM glyphs for canvas glyphs, atomically within the current task. */
|
|
1088
|
+
maybeConceal() {
|
|
1089
|
+
if (!this.revealed || this.lines.length === 0) return;
|
|
1090
|
+
if (this.done < this.lines.length) return;
|
|
1091
|
+
for (const t of this.tiles) if (t.visible && t.dirtyFrom < this.tileEnd(t)) return;
|
|
1092
|
+
const { codeEl, overlayEl } = this.input;
|
|
1093
|
+
this.revealed = false;
|
|
1094
|
+
codeEl.style.color = "transparent";
|
|
1095
|
+
overlayEl.style.visibility = "visible";
|
|
1096
|
+
}
|
|
1097
|
+
reveal() {
|
|
1098
|
+
if (this.revealed) return;
|
|
1099
|
+
this.revealed = true;
|
|
1100
|
+
const { codeEl, overlayEl } = this.input;
|
|
1101
|
+
codeEl.style.color = "";
|
|
1102
|
+
overlayEl.style.visibility = "hidden";
|
|
1103
|
+
}
|
|
1104
|
+
tileEnd(t) {
|
|
1105
|
+
return Math.min(t.start + TILE_LINES, this.lines.length);
|
|
1106
|
+
}
|
|
1107
|
+
layoutTiles() {
|
|
1108
|
+
const { overlayEl, lineHeight } = this.input;
|
|
1109
|
+
const count = Math.ceil(this.lines.length / TILE_LINES);
|
|
1110
|
+
while (this.tiles.length > count) {
|
|
1111
|
+
const t = this.tiles.pop();
|
|
1112
|
+
this.io?.unobserve(t.canvas);
|
|
1113
|
+
t.canvas.remove();
|
|
1114
|
+
}
|
|
1115
|
+
while (this.tiles.length < count) {
|
|
1116
|
+
const start = this.tiles.length * TILE_LINES;
|
|
1117
|
+
const canvas = document.createElement("canvas");
|
|
1118
|
+
canvas.width = 0;
|
|
1119
|
+
canvas.height = 0;
|
|
1120
|
+
Object.assign(canvas.style, {
|
|
1121
|
+
position: "absolute",
|
|
1122
|
+
left: "0px",
|
|
1123
|
+
top: `${start * lineHeight}px`,
|
|
1124
|
+
width: "0px",
|
|
1125
|
+
height: "0px",
|
|
1126
|
+
pointerEvents: "none"
|
|
1127
|
+
});
|
|
1128
|
+
overlayEl.appendChild(canvas);
|
|
1129
|
+
const tile = {
|
|
1130
|
+
canvas,
|
|
1131
|
+
start,
|
|
1132
|
+
dirtyFrom: start,
|
|
1133
|
+
visible: false,
|
|
1134
|
+
cssWidth: 0,
|
|
1135
|
+
dpr: 0
|
|
1136
|
+
};
|
|
1137
|
+
this.tiles.push(tile);
|
|
1138
|
+
this.io?.observe(canvas);
|
|
1139
|
+
}
|
|
1140
|
+
for (const t of this.tiles) t.canvas.style.top = `${t.start * lineHeight}px`;
|
|
1141
|
+
}
|
|
1142
|
+
ensureMetrics(ctx) {
|
|
1143
|
+
if (this.metrics != null) return this.metrics;
|
|
1144
|
+
const { font } = this.input;
|
|
1145
|
+
ctx.font = font;
|
|
1146
|
+
const probe = ctx.measureText("Mg");
|
|
1147
|
+
const sizeMatch = /(\d+(?:\.\d+)?)px/.exec(font);
|
|
1148
|
+
const size = sizeMatch != null ? parseFloat(sizeMatch[1]) : 16;
|
|
1149
|
+
this.metrics = {
|
|
1150
|
+
ascent: probe.fontBoundingBoxAscent ?? size * .8,
|
|
1151
|
+
descent: probe.fontBoundingBoxDescent ?? size * .2,
|
|
1152
|
+
tabAdvance: ctx.measureText(" ").width * 8
|
|
1153
|
+
};
|
|
1154
|
+
return this.metrics;
|
|
1155
|
+
}
|
|
1156
|
+
ensurePlainColor() {
|
|
1157
|
+
if (this.plainColor != null) return this.plainColor;
|
|
1158
|
+
const { codeEl } = this.input;
|
|
1159
|
+
const inline = codeEl.style.color;
|
|
1160
|
+
if (!this.revealed) codeEl.style.color = "";
|
|
1161
|
+
this.plainColor = getComputedStyle(codeEl).color || "#888";
|
|
1162
|
+
if (!this.revealed) codeEl.style.color = inline;
|
|
1163
|
+
return this.plainColor;
|
|
1164
|
+
}
|
|
1165
|
+
/** Draw (or just measure, when `y` is null) `text` from `x`, honouring tabs. */
|
|
1166
|
+
runText(ctx, text, x, tabAdvance, y) {
|
|
1167
|
+
let from = 0;
|
|
1168
|
+
for (;;) {
|
|
1169
|
+
const t = text.indexOf(" ", from);
|
|
1170
|
+
const seg = t < 0 ? text.slice(from) : text.slice(from, t);
|
|
1171
|
+
if (seg.length > 0) {
|
|
1172
|
+
if (y != null) ctx.fillText(seg, x, y);
|
|
1173
|
+
x += ctx.measureText(seg).width;
|
|
1174
|
+
}
|
|
1175
|
+
if (t < 0) return x;
|
|
1176
|
+
x = (Math.floor(x / tabAdvance + 1e-6) + 1) * tabAdvance;
|
|
1177
|
+
from = t + 1;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
paintTile(t) {
|
|
1181
|
+
const { lineHeight, font, colors } = this.input;
|
|
1182
|
+
const mctx = measureCtx();
|
|
1183
|
+
mctx.font = font;
|
|
1184
|
+
const m = this.ensureMetrics(mctx);
|
|
1185
|
+
const end = this.tileEnd(t);
|
|
1186
|
+
let maxW = 0;
|
|
1187
|
+
for (let i = t.start; i < end; i++) {
|
|
1188
|
+
const w = this.widths[i] ??= this.runText(mctx, this.lines[i], 0, m.tabAdvance, null);
|
|
1189
|
+
if (w > maxW) maxW = w;
|
|
1190
|
+
}
|
|
1191
|
+
const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
1192
|
+
const cssH = (end - t.start) * lineHeight;
|
|
1193
|
+
let from = Math.max(t.dirtyFrom, t.start);
|
|
1194
|
+
const needResize = t.dpr !== dpr || maxW > t.cssWidth || from <= t.start || Math.ceil(cssH * dpr) !== t.canvas.height;
|
|
1195
|
+
if (needResize) {
|
|
1196
|
+
from = t.start;
|
|
1197
|
+
t.cssWidth = maxW;
|
|
1198
|
+
t.dpr = dpr;
|
|
1199
|
+
t.canvas.width = Math.ceil(maxW * dpr);
|
|
1200
|
+
t.canvas.height = Math.ceil(cssH * dpr);
|
|
1201
|
+
t.canvas.style.width = `${maxW}px`;
|
|
1202
|
+
t.canvas.style.height = `${cssH}px`;
|
|
1203
|
+
}
|
|
1204
|
+
const ctx = t.canvas.getContext("2d");
|
|
1205
|
+
if (ctx == null) {
|
|
1206
|
+
t.dirtyFrom = end;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1210
|
+
ctx.font = font;
|
|
1211
|
+
ctx.textBaseline = "alphabetic";
|
|
1212
|
+
if (!needResize) {
|
|
1213
|
+
const yTop = (from - t.start) * lineHeight;
|
|
1214
|
+
ctx.clearRect(0, yTop, t.cssWidth + 1, cssH - yTop);
|
|
1215
|
+
}
|
|
1216
|
+
const plain = this.ensurePlainColor();
|
|
1217
|
+
const half = (lineHeight - (m.ascent + m.descent)) / 2;
|
|
1218
|
+
for (let i = from; i < end; i++) {
|
|
1219
|
+
const y = (i - t.start) * lineHeight + half + m.ascent;
|
|
1220
|
+
this.paintLine(ctx, this.lines[i], this.tokens[i], y, m.tabAdvance, colors, plain);
|
|
1221
|
+
}
|
|
1222
|
+
t.dirtyFrom = end;
|
|
1223
|
+
}
|
|
1224
|
+
paintLine(ctx, line, tokens, y, tabAdvance, colors, plain) {
|
|
1225
|
+
let x = 0;
|
|
1226
|
+
let pos = 0;
|
|
1227
|
+
let fill = "";
|
|
1228
|
+
const setFill = (c) => {
|
|
1229
|
+
if (c !== fill) {
|
|
1230
|
+
fill = c;
|
|
1231
|
+
ctx.fillStyle = c;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
for (let k = 0; k <= tokens.length; k++) {
|
|
1235
|
+
const tok = k < tokens.length ? tokens[k] : null;
|
|
1236
|
+
const gapEnd = tok != null ? tok.start : line.length;
|
|
1237
|
+
if (gapEnd > pos) {
|
|
1238
|
+
setFill(plain);
|
|
1239
|
+
x = this.runText(ctx, line.slice(pos, gapEnd), x, tabAdvance, y);
|
|
1240
|
+
pos = gapEnd;
|
|
1241
|
+
}
|
|
1242
|
+
if (tok == null) break;
|
|
1243
|
+
const c = colors[tok.type];
|
|
1244
|
+
setFill(c === "currentColor" || c === "inherit" ? plain : c);
|
|
1245
|
+
x = this.runText(ctx, line.slice(tok.start, tok.end), x, tabAdvance, y);
|
|
1246
|
+
pos = tok.end;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
listen() {
|
|
1250
|
+
const onSignal = () => this.syncVisibility();
|
|
1251
|
+
const { codeEl } = this.input;
|
|
1252
|
+
this.scrollers = [];
|
|
1253
|
+
for (let el = codeEl.parentElement; el != null; el = el.parentElement) {
|
|
1254
|
+
const s = getComputedStyle(el);
|
|
1255
|
+
const o = `${s.overflowY} ${s.overflowX}`;
|
|
1256
|
+
if (o.includes("auto") || o.includes("scroll") || o.includes("overlay")) {
|
|
1257
|
+
this.scrollers.push(el);
|
|
1258
|
+
el.addEventListener("scroll", onSignal, { passive: true });
|
|
1259
|
+
const target = el;
|
|
1260
|
+
this.unlisten.push(() => target.removeEventListener("scroll", onSignal));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
window.addEventListener("scroll", onSignal, { passive: true });
|
|
1264
|
+
window.addEventListener("resize", onSignal);
|
|
1265
|
+
this.unlisten.push(() => {
|
|
1266
|
+
window.removeEventListener("scroll", onSignal);
|
|
1267
|
+
window.removeEventListener("resize", onSignal);
|
|
1268
|
+
});
|
|
1269
|
+
if (typeof IntersectionObserver !== "undefined") this.io = new IntersectionObserver(onSignal, { rootMargin: "50%" });
|
|
1270
|
+
const fonts = typeof document !== "undefined" ? document.fonts : void 0;
|
|
1271
|
+
if (fonts != null && typeof fonts.addEventListener === "function") {
|
|
1272
|
+
const onFonts = () => {
|
|
1273
|
+
this.metrics = null;
|
|
1274
|
+
this.widths.length = 0;
|
|
1275
|
+
for (const t of this.tiles) {
|
|
1276
|
+
t.dirtyFrom = t.start;
|
|
1277
|
+
t.dpr = 0;
|
|
1278
|
+
}
|
|
1279
|
+
this.syncVisibility();
|
|
1280
|
+
};
|
|
1281
|
+
fonts.addEventListener("loadingdone", onFonts);
|
|
1282
|
+
this.unlisten.push(() => fonts.removeEventListener("loadingdone", onFonts));
|
|
1283
|
+
}
|
|
1284
|
+
this.watchDpr();
|
|
1285
|
+
}
|
|
1286
|
+
watchDpr() {
|
|
1287
|
+
if (typeof matchMedia !== "function" || typeof devicePixelRatio !== "number") return;
|
|
1288
|
+
const mq = matchMedia(`(resolution: ${devicePixelRatio}dppx)`);
|
|
1289
|
+
if (typeof mq.addEventListener !== "function") return;
|
|
1290
|
+
const onChange = () => {
|
|
1291
|
+
stop();
|
|
1292
|
+
this.unlisten = this.unlisten.filter((u) => u !== stop);
|
|
1293
|
+
for (const t of this.tiles) t.dirtyFrom = t.start;
|
|
1294
|
+
this.syncVisibility();
|
|
1295
|
+
this.watchDpr();
|
|
1296
|
+
};
|
|
1297
|
+
const stop = () => mq.removeEventListener("change", onChange);
|
|
1298
|
+
mq.addEventListener("change", onChange);
|
|
1299
|
+
this.unlisten.push(stop);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/primitives/code-block.tsx
|
|
1304
|
+
function lineCount(value) {
|
|
1305
|
+
if (value.length === 0) return 0;
|
|
1306
|
+
let lines = 1;
|
|
1307
|
+
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 10) lines++;
|
|
1308
|
+
if (value.charCodeAt(value.length - 1) === 10) lines--;
|
|
1309
|
+
return Math.max(1, lines);
|
|
1310
|
+
}
|
|
1311
|
+
function measureCodeBlock(props, ctx) {
|
|
1312
|
+
(0, _wingleeio_mugen.assertMeasurableFont)(props.font);
|
|
1313
|
+
const pad = props.padding ?? 0;
|
|
1314
|
+
return lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1315
|
+
}
|
|
1316
|
+
const colorsCache = /* @__PURE__ */ new WeakMap();
|
|
1317
|
+
function resolveTokenColors(overrides) {
|
|
1318
|
+
if (overrides == null) return defaultTokenColors;
|
|
1319
|
+
let full = colorsCache.get(overrides);
|
|
1320
|
+
if (full === void 0) {
|
|
1321
|
+
full = {
|
|
1322
|
+
...defaultTokenColors,
|
|
1323
|
+
...overrides
|
|
1324
|
+
};
|
|
1325
|
+
colorsCache.set(overrides, full);
|
|
1326
|
+
}
|
|
1327
|
+
return full;
|
|
1328
|
+
}
|
|
1329
|
+
const useIsoLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
|
|
1330
|
+
function HighlightedCode(props) {
|
|
1331
|
+
const codeRef = (0, react.useRef)(null);
|
|
1332
|
+
const overlayRef = (0, react.useRef)(null);
|
|
1333
|
+
const sessionRef = (0, react.useRef)(null);
|
|
1334
|
+
const { value, font, lineHeight, profile, colors } = props;
|
|
1335
|
+
useIsoLayoutEffect(() => {
|
|
1336
|
+
const codeEl = codeRef.current;
|
|
1337
|
+
const overlayEl = overlayRef.current;
|
|
1338
|
+
if (codeEl == null || overlayEl == null) return;
|
|
1339
|
+
(sessionRef.current ??= new HighlightSession()).update({
|
|
1340
|
+
codeEl,
|
|
1341
|
+
overlayEl,
|
|
1342
|
+
value,
|
|
1343
|
+
font,
|
|
1344
|
+
lineHeight,
|
|
1345
|
+
profile,
|
|
1346
|
+
colors
|
|
1347
|
+
});
|
|
1348
|
+
}, [
|
|
1349
|
+
value,
|
|
1350
|
+
font,
|
|
1351
|
+
lineHeight,
|
|
1352
|
+
profile,
|
|
1353
|
+
colors
|
|
1354
|
+
]);
|
|
1355
|
+
(0, react.useEffect)(() => () => {
|
|
1356
|
+
sessionRef.current?.destroy();
|
|
1357
|
+
sessionRef.current = null;
|
|
1358
|
+
}, []);
|
|
1359
|
+
const overlayStyle = {
|
|
1360
|
+
position: "absolute",
|
|
1361
|
+
top: `${props.padding}px`,
|
|
1362
|
+
left: `${props.padding}px`,
|
|
1363
|
+
width: 0,
|
|
1364
|
+
height: 0,
|
|
1365
|
+
visibility: "hidden",
|
|
1366
|
+
pointerEvents: "none"
|
|
1367
|
+
};
|
|
1368
|
+
return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("code", {
|
|
1369
|
+
ref: codeRef,
|
|
1370
|
+
style: props.codeStyle,
|
|
1371
|
+
...props.lang != null ? { "data-lang": props.lang } : null
|
|
1372
|
+
}, props.value), (0, react.createElement)("div", {
|
|
1373
|
+
ref: overlayRef,
|
|
1374
|
+
"aria-hidden": true,
|
|
1375
|
+
style: overlayStyle
|
|
1376
|
+
}));
|
|
1377
|
+
}
|
|
1378
|
+
function renderCodeBlock(props) {
|
|
1379
|
+
const pad = props.padding ?? 0;
|
|
1380
|
+
const profile = props.highlight === false ? null : profileFor(props.lang);
|
|
1381
|
+
const preStyle = {
|
|
1382
|
+
margin: 0,
|
|
1383
|
+
padding: `${pad}px`,
|
|
1384
|
+
overflowX: "auto",
|
|
1385
|
+
font: (0, _wingleeio_mugen.fontWithLineHeight)(props.font, props.lineHeight),
|
|
1386
|
+
boxSizing: "border-box",
|
|
1387
|
+
...props.background != null ? { background: props.background } : null,
|
|
1388
|
+
...props.color != null ? { color: props.color } : null,
|
|
1389
|
+
...props.radius != null ? { borderRadius: `${props.radius}px` } : null,
|
|
1390
|
+
...profile != null ? {
|
|
1391
|
+
position: "relative",
|
|
1392
|
+
tabSize: 8
|
|
1393
|
+
} : null,
|
|
1394
|
+
...props.style
|
|
1395
|
+
};
|
|
1396
|
+
const codeStyle = {
|
|
1397
|
+
font: "inherit",
|
|
1398
|
+
whiteSpace: "pre",
|
|
1399
|
+
margin: 0,
|
|
1400
|
+
padding: 0
|
|
1401
|
+
};
|
|
1402
|
+
if (profile == null) return (0, react.createElement)("pre", {
|
|
1403
|
+
className: props.className,
|
|
1404
|
+
style: preStyle
|
|
1405
|
+
}, (0, react.createElement)("code", {
|
|
1406
|
+
style: codeStyle,
|
|
1407
|
+
...props.lang ? { "data-lang": props.lang } : null
|
|
1408
|
+
}, props.value));
|
|
1409
|
+
return (0, react.createElement)("pre", {
|
|
1410
|
+
className: props.className,
|
|
1411
|
+
style: preStyle
|
|
1412
|
+
}, (0, react.createElement)(HighlightedCode, {
|
|
1413
|
+
value: props.value,
|
|
1414
|
+
lang: props.lang,
|
|
1415
|
+
font: props.font,
|
|
1416
|
+
lineHeight: props.lineHeight,
|
|
1417
|
+
padding: pad,
|
|
1418
|
+
profile,
|
|
1419
|
+
colors: resolveTokenColors(props.highlight === false ? void 0 : props.highlight),
|
|
1420
|
+
codeStyle
|
|
1421
|
+
}));
|
|
1422
|
+
}
|
|
1423
|
+
/** A measurable fenced-code primitive (no wrapping; height from line count). */
|
|
1424
|
+
const CodeBlock = (0, _wingleeio_mugen.markPrimitive)(renderCodeBlock, {
|
|
1425
|
+
name: "CodeBlock",
|
|
1426
|
+
measure: (props, ctx) => measureCodeBlock(props, ctx)
|
|
1427
|
+
});
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/primitives/table-block.tsx
|
|
1430
|
+
/**
|
|
1431
|
+
* Floor for a column's content share, so a short column ("1k") beside a prose
|
|
1432
|
+
* column keeps a readable width instead of wrapping per character.
|
|
1433
|
+
*/
|
|
1434
|
+
const MIN_COLUMN_CONTENT = 48;
|
|
1435
|
+
const STUB_CTX = {
|
|
1436
|
+
defaults: {},
|
|
1437
|
+
width: 0,
|
|
1438
|
+
measure: () => 0
|
|
1439
|
+
};
|
|
1440
|
+
function columnCount(rows) {
|
|
1441
|
+
let n = 0;
|
|
1442
|
+
for (const row of rows) n = Math.max(n, row.length);
|
|
1443
|
+
return n;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Per-column flex ratios: max-content width of the column (plus padding), or
|
|
1447
|
+
* `null` when some cell's natural width is unknowable (→ equal columns).
|
|
1448
|
+
*/
|
|
1449
|
+
function columnRatios(p, ctx) {
|
|
1450
|
+
const cols = columnCount(p.rows);
|
|
1451
|
+
const ratios = new Array(cols).fill(MIN_COLUMN_CONTENT);
|
|
1452
|
+
for (const row of p.rows) for (let c = 0; c < row.length; c++) {
|
|
1453
|
+
const cell = row[c];
|
|
1454
|
+
if (cell == null) continue;
|
|
1455
|
+
let w;
|
|
1456
|
+
try {
|
|
1457
|
+
w = (0, _wingleeio_mugen.naturalWidthOf)(cell, ctx);
|
|
1458
|
+
} catch {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
if (w == null) return null;
|
|
1462
|
+
if (w > ratios[c]) ratios[c] = w;
|
|
1463
|
+
}
|
|
1464
|
+
return ratios.map((r) => r + 2 * p.cellPadding);
|
|
1465
|
+
}
|
|
1466
|
+
function ratiosOrEqual(p, ctx) {
|
|
1467
|
+
return columnRatios(p, ctx) ?? new Array(columnCount(p.rows)).fill(1);
|
|
1468
|
+
}
|
|
1469
|
+
function measureTable(p, ctx) {
|
|
1470
|
+
const rows = p.rows;
|
|
1471
|
+
const cols = columnCount(rows);
|
|
1472
|
+
if (rows.length === 0 || cols === 0) return 0;
|
|
1473
|
+
const ratios = ratiosOrEqual(p, ctx);
|
|
1474
|
+
const total = ratios.reduce((a, b) => a + b, 0);
|
|
1475
|
+
let height = (rows.length - 1) * p.divider;
|
|
1476
|
+
for (const row of rows) {
|
|
1477
|
+
let rowH = 2 * p.cellPadding;
|
|
1478
|
+
for (let c = 0; c < cols; c++) {
|
|
1479
|
+
const cell = row[c];
|
|
1480
|
+
if (cell == null) continue;
|
|
1481
|
+
const colW = total > 0 ? ctx.width * ratios[c] / total : 0;
|
|
1482
|
+
const inner = Math.max(0, colW - 2 * p.cellPadding);
|
|
1483
|
+
rowH = Math.max(rowH, ctx.measure(cell, inner) + 2 * p.cellPadding);
|
|
1484
|
+
}
|
|
1485
|
+
height += rowH;
|
|
1486
|
+
}
|
|
1487
|
+
return height;
|
|
1488
|
+
}
|
|
1489
|
+
function renderTableBlock(p) {
|
|
1490
|
+
const cols = columnCount(p.rows);
|
|
1491
|
+
const ratios = ratiosOrEqual(p, STUB_CTX);
|
|
1492
|
+
const outer = {
|
|
1493
|
+
display: "flex",
|
|
1494
|
+
flexDirection: "column",
|
|
1495
|
+
margin: 0,
|
|
1496
|
+
...p.radius > 0 ? {
|
|
1497
|
+
borderRadius: `${p.radius}px`,
|
|
1498
|
+
overflow: "hidden"
|
|
1499
|
+
} : null,
|
|
1500
|
+
...p.divider > 0 ? { boxShadow: `inset 0 0 0 ${p.divider}px ${p.borderColor}` } : null,
|
|
1501
|
+
...p.style
|
|
1502
|
+
};
|
|
1503
|
+
const children = [];
|
|
1504
|
+
p.rows.forEach((row, r) => {
|
|
1505
|
+
if (r > 0 && p.divider > 0) children.push((0, react.createElement)("div", {
|
|
1506
|
+
key: `d${r}`,
|
|
1507
|
+
style: {
|
|
1508
|
+
height: `${p.divider}px`,
|
|
1509
|
+
background: p.borderColor,
|
|
1510
|
+
flex: "none"
|
|
1511
|
+
}
|
|
1512
|
+
}));
|
|
1513
|
+
children.push((0, react.createElement)("div", {
|
|
1514
|
+
key: r,
|
|
1515
|
+
style: {
|
|
1516
|
+
display: "flex",
|
|
1517
|
+
...r === 0 ? { background: p.headerBackground } : null
|
|
1518
|
+
}
|
|
1519
|
+
}, Array.from({ length: cols }, (_, c) => (0, react.createElement)("div", {
|
|
1520
|
+
key: c,
|
|
1521
|
+
style: {
|
|
1522
|
+
flex: `${ratios[c]} ${ratios[c]} 0px`,
|
|
1523
|
+
minWidth: 0,
|
|
1524
|
+
boxSizing: "border-box",
|
|
1525
|
+
padding: `${p.cellPadding}px`
|
|
1526
|
+
}
|
|
1527
|
+
}, row[c] ?? null))));
|
|
1528
|
+
});
|
|
1529
|
+
return (0, react.createElement)("div", {
|
|
1530
|
+
className: p.className,
|
|
1531
|
+
style: outer
|
|
1532
|
+
}, children);
|
|
1533
|
+
}
|
|
1534
|
+
/** A measurable GFM-table primitive with aligned, content-proportional columns. */
|
|
1535
|
+
const TableBlock = (0, _wingleeio_mugen.markPrimitive)(renderTableBlock, {
|
|
1536
|
+
name: "TableBlock",
|
|
1537
|
+
measure: (props, ctx) => measureTable(props, ctx),
|
|
1538
|
+
naturalWidth: (props, ctx) => {
|
|
1539
|
+
const ratios = columnRatios(props, ctx);
|
|
1540
|
+
if (ratios == null) return null;
|
|
1541
|
+
return ratios.reduce((a, b) => a + b, 0);
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
//#endregion
|
|
1545
|
+
//#region src/components.tsx
|
|
1546
|
+
/** A `<blockquote>`-backed vertical box, so the default quote is semantic. */
|
|
1547
|
+
const Blockquote = (0, _wingleeio_mugen.definePrimitive)("blockquote", { name: "Blockquote" });
|
|
1548
|
+
function bodyFont(theme) {
|
|
1549
|
+
return `${theme.fontSize}px ${theme.fontFamily}`;
|
|
1550
|
+
}
|
|
1551
|
+
function renderList(node, ctx) {
|
|
1552
|
+
const theme = ctx.theme;
|
|
1553
|
+
const ordered = node.ordered === true;
|
|
1554
|
+
const start = node.start ?? 1;
|
|
1555
|
+
const font = bodyFont(theme);
|
|
1556
|
+
const markerColor = theme.list.markerColor !== "inherit" ? theme.list.markerColor : void 0;
|
|
1557
|
+
const items = node.children.map((item, i) => {
|
|
1558
|
+
const markerText = item.checked != null ? item.checked ? "☑" : "☐" : ordered ? `${start + i}.` : "•";
|
|
1559
|
+
return ctx.memo(item, `li:${i}:${markerText}`, () => {
|
|
1560
|
+
const content = ctx.renderBlocks(item.children, theme.list.gap);
|
|
1561
|
+
const markerRun = {
|
|
1562
|
+
text: markerText,
|
|
1563
|
+
font
|
|
1564
|
+
};
|
|
1565
|
+
if (markerColor != null) markerRun.color = markerColor;
|
|
1566
|
+
const marker = (0, react.createElement)(RichText, {
|
|
1567
|
+
runs: [markerRun],
|
|
1568
|
+
font,
|
|
1569
|
+
lineHeight: theme.lineHeight
|
|
1570
|
+
});
|
|
1571
|
+
return (0, react.createElement)(_wingleeio_mugen.HStack, {
|
|
1572
|
+
key: i,
|
|
1573
|
+
align: "flex-start"
|
|
1574
|
+
}, (0, react.createElement)(_wingleeio_mugen.VStack, { width: theme.list.indent }, marker), (0, react.createElement)(_wingleeio_mugen.VStack, {}, content));
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
return (0, react.createElement)(_wingleeio_mugen.VStack, { gap: theme.list.gap }, items);
|
|
1578
|
+
}
|
|
1579
|
+
function renderTable(node, ctx) {
|
|
1580
|
+
const theme = ctx.theme;
|
|
1581
|
+
const align = node.align ?? [];
|
|
1582
|
+
return (0, react.createElement)(TableBlock, {
|
|
1583
|
+
rows: node.children.map((row, ri) => row.children.map((cell, ci) => ctx.memo(cell, `td:${ri}:${ci}:${ri === 0 ? "h" : ""}:${align[ci] ?? ""}`, () => ctx.inlineText(cell.children, {
|
|
1584
|
+
lineHeight: theme.lineHeight,
|
|
1585
|
+
weight: ri === 0 ? theme.table.headerWeight : void 0,
|
|
1586
|
+
align: align[ci] ?? void 0
|
|
1587
|
+
})))),
|
|
1588
|
+
cellPadding: theme.table.cellPadding,
|
|
1589
|
+
divider: theme.table.gap,
|
|
1590
|
+
borderColor: theme.table.borderColor,
|
|
1591
|
+
headerBackground: theme.table.headerBackground,
|
|
1592
|
+
radius: theme.table.radius
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
function renderImage(node, ctx) {
|
|
1596
|
+
const theme = ctx.theme;
|
|
1597
|
+
if (theme.image.placeholderHeight > 0) return (0, react.createElement)(_wingleeio_mugen.VStack, { height: theme.image.placeholderHeight });
|
|
1598
|
+
const font = bodyFont(theme);
|
|
1599
|
+
return (0, react.createElement)(RichText, {
|
|
1600
|
+
runs: [{
|
|
1601
|
+
text: `\u{1F5BC} ${node.alt || node.title || node.url}`,
|
|
1602
|
+
font,
|
|
1603
|
+
color: theme.image.color
|
|
1604
|
+
}],
|
|
1605
|
+
font,
|
|
1606
|
+
lineHeight: theme.lineHeight,
|
|
1607
|
+
color: theme.image.color
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
const defaultComponents = {
|
|
1611
|
+
paragraph: ({ children }) => children,
|
|
1612
|
+
heading: ({ children }) => children,
|
|
1613
|
+
blockquote: ({ children, ctx }) => {
|
|
1614
|
+
const bq = ctx.theme.blockquote;
|
|
1615
|
+
return (0, react.createElement)(Blockquote, {
|
|
1616
|
+
padding: bq.padding,
|
|
1617
|
+
style: {
|
|
1618
|
+
boxShadow: `inset ${bq.borderWidth}px 0 0 ${bq.borderColor}`,
|
|
1619
|
+
...bq.color !== "inherit" ? { color: bq.color } : null
|
|
1620
|
+
}
|
|
1621
|
+
}, children);
|
|
1622
|
+
},
|
|
1623
|
+
code: ({ node, ctx }) => {
|
|
1624
|
+
const c = ctx.theme.code;
|
|
1625
|
+
const font = `${c.fontSize}px ${ctx.theme.monoFamily}`;
|
|
1626
|
+
return (0, react.createElement)(CodeBlock, {
|
|
1627
|
+
value: node.value,
|
|
1628
|
+
...node.lang ? { lang: node.lang } : null,
|
|
1629
|
+
font,
|
|
1630
|
+
lineHeight: c.lineHeight,
|
|
1631
|
+
padding: c.padding,
|
|
1632
|
+
background: c.background,
|
|
1633
|
+
radius: c.radius,
|
|
1634
|
+
highlight: c.highlight,
|
|
1635
|
+
...c.color !== "inherit" ? { color: c.color } : null
|
|
1636
|
+
});
|
|
1637
|
+
},
|
|
1638
|
+
list: ({ node, ctx }) => renderList(node, ctx),
|
|
1639
|
+
table: ({ node, ctx }) => renderTable(node, ctx),
|
|
1640
|
+
image: ({ node, ctx }) => renderImage(node, ctx),
|
|
1641
|
+
thematicBreak: ({ ctx }) => {
|
|
1642
|
+
const r = ctx.theme.rule;
|
|
1643
|
+
return (0, react.createElement)(_wingleeio_mugen.VStack, { padding: r.gap }, (0, react.createElement)(_wingleeio_mugen.VStack, {
|
|
1644
|
+
height: r.thickness,
|
|
1645
|
+
style: { background: r.color }
|
|
1646
|
+
}));
|
|
1647
|
+
},
|
|
1648
|
+
html: () => null
|
|
1649
|
+
};
|
|
1650
|
+
const mergeCache = /* @__PURE__ */ new WeakMap();
|
|
1651
|
+
/** Merge a partial component set over the defaults. */
|
|
1652
|
+
function mergeComponents(components) {
|
|
1653
|
+
if (components == null) return defaultComponents;
|
|
1654
|
+
const cached = mergeCache.get(components);
|
|
1655
|
+
if (cached !== void 0) return cached;
|
|
1656
|
+
const merged = {
|
|
1657
|
+
...defaultComponents,
|
|
1658
|
+
...components
|
|
1659
|
+
};
|
|
1660
|
+
mergeCache.set(components, merged);
|
|
1661
|
+
return merged;
|
|
1662
|
+
}
|
|
1663
|
+
//#endregion
|
|
1664
|
+
//#region src/render.tsx
|
|
1665
|
+
/** The default inner rendering for a node — inline `RichText` or rendered child blocks. */
|
|
1666
|
+
function renderChildren(node, ctx) {
|
|
1667
|
+
const theme = ctx.theme;
|
|
1668
|
+
switch (node.type) {
|
|
1669
|
+
case "paragraph": return ctx.inlineText(node.children, { lineHeight: theme.lineHeight });
|
|
1670
|
+
case "heading": {
|
|
1671
|
+
const d = node.depth;
|
|
1672
|
+
return ctx.inlineText(node.children, {
|
|
1673
|
+
size: theme.heading.sizes[d],
|
|
1674
|
+
weight: theme.heading.weight,
|
|
1675
|
+
lineHeight: theme.heading.lineHeights[d],
|
|
1676
|
+
...theme.heading.color !== "inherit" ? { color: theme.heading.color } : null
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
case "blockquote": return ctx.renderBlocks(node.children, theme.blockquote.gap);
|
|
1680
|
+
default: return null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const blockCache = /* @__PURE__ */ new WeakMap();
|
|
1684
|
+
const MAX_CONTENT_CACHE = 4096;
|
|
1685
|
+
const contentCache = /* @__PURE__ */ new Map();
|
|
1686
|
+
let optIdSeq = 0;
|
|
1687
|
+
const optId = /* @__PURE__ */ new WeakMap();
|
|
1688
|
+
function idOf(o) {
|
|
1689
|
+
let id = optId.get(o);
|
|
1690
|
+
if (id === void 0) {
|
|
1691
|
+
id = ++optIdSeq;
|
|
1692
|
+
optId.set(o, id);
|
|
1693
|
+
}
|
|
1694
|
+
return id;
|
|
1695
|
+
}
|
|
1696
|
+
/** Structural signature of a node — content + shape, position-independent. */
|
|
1697
|
+
function blockSig(node) {
|
|
1698
|
+
return JSON.stringify(node, (k, v) => k === "position" ? void 0 : v);
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Memoize an element by its node's content. `variant` distinguishes renderings
|
|
1702
|
+
* of the same node that differ by sibling context (a list item's index/marker, a
|
|
1703
|
+
* table cell's column). First tries the node-reference cache (completed blocks
|
|
1704
|
+
* outside the re-parsed tail), then the content signature (unchanged sub-blocks
|
|
1705
|
+
* inside the streaming block), and only then runs `build`.
|
|
1706
|
+
*/
|
|
1707
|
+
function memoElement(node, ctx, variant, build) {
|
|
1708
|
+
const byRef = blockCache.get(node);
|
|
1709
|
+
if (byRef !== void 0 && byRef.theme === ctx.theme && byRef.components === ctx.components && byRef.variant === variant) return byRef.el;
|
|
1710
|
+
const sigKey = `${idOf(ctx.theme)}:${idOf(ctx.components)}:${variant}:${blockSig(node)}`;
|
|
1711
|
+
const byContent = contentCache.get(sigKey);
|
|
1712
|
+
if (byContent !== void 0) {
|
|
1713
|
+
contentCache.delete(sigKey);
|
|
1714
|
+
contentCache.set(sigKey, byContent);
|
|
1715
|
+
blockCache.set(node, {
|
|
1716
|
+
el: byContent,
|
|
1717
|
+
theme: ctx.theme,
|
|
1718
|
+
components: ctx.components,
|
|
1719
|
+
variant
|
|
1720
|
+
});
|
|
1721
|
+
return byContent;
|
|
1722
|
+
}
|
|
1723
|
+
const el = build();
|
|
1724
|
+
blockCache.set(node, {
|
|
1725
|
+
el,
|
|
1726
|
+
theme: ctx.theme,
|
|
1727
|
+
components: ctx.components,
|
|
1728
|
+
variant
|
|
1729
|
+
});
|
|
1730
|
+
if (contentCache.size >= MAX_CONTENT_CACHE) {
|
|
1731
|
+
const oldest = contentCache.keys().next().value;
|
|
1732
|
+
if (oldest !== void 0) contentCache.delete(oldest);
|
|
1733
|
+
}
|
|
1734
|
+
contentCache.set(sigKey, el);
|
|
1735
|
+
return el;
|
|
1736
|
+
}
|
|
1737
|
+
function memoDispatch(node, ctx, key) {
|
|
1738
|
+
return memoElement(node, ctx, `b:${key}`, () => dispatch(node, ctx, key));
|
|
1739
|
+
}
|
|
1740
|
+
function dispatch(node, ctx, key) {
|
|
1741
|
+
if (node.type === "paragraph" && node.children.length === 1 && node.children[0]?.type === "image") {
|
|
1742
|
+
const image = node.children[0];
|
|
1743
|
+
return (0, react.createElement)(ctx.components.image, {
|
|
1744
|
+
key,
|
|
1745
|
+
node: image,
|
|
1746
|
+
ctx,
|
|
1747
|
+
children: null
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
const Comp = ctx.components[node.type];
|
|
1751
|
+
if (Comp == null) return null;
|
|
1752
|
+
return (0, react.createElement)(Comp, {
|
|
1753
|
+
key,
|
|
1754
|
+
node,
|
|
1755
|
+
ctx,
|
|
1756
|
+
children: renderChildren(node, ctx)
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
function createContext(theme, components) {
|
|
1760
|
+
const ctx = {
|
|
1761
|
+
theme,
|
|
1762
|
+
components,
|
|
1763
|
+
renderBlocks(nodes, gap = theme.blockGap) {
|
|
1764
|
+
const kids = [];
|
|
1765
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1766
|
+
const el = ctx.renderBlock(nodes[i], i);
|
|
1767
|
+
if (el != null) kids.push(el);
|
|
1768
|
+
}
|
|
1769
|
+
if (kids.length === 0) return null;
|
|
1770
|
+
return (0, react.createElement)(_wingleeio_mugen.VStack, { gap }, kids);
|
|
1771
|
+
},
|
|
1772
|
+
renderBlock(node, key) {
|
|
1773
|
+
return memoDispatch(node, ctx, key);
|
|
1774
|
+
},
|
|
1775
|
+
memo(node, variant, build) {
|
|
1776
|
+
return memoElement(node, ctx, variant, build);
|
|
1777
|
+
},
|
|
1778
|
+
renderChildren(node) {
|
|
1779
|
+
return renderChildren(node, ctx);
|
|
1780
|
+
},
|
|
1781
|
+
inlineRuns(nodes, base) {
|
|
1782
|
+
const fmt = {
|
|
1783
|
+
...baseFormat(theme),
|
|
1784
|
+
...base
|
|
1785
|
+
};
|
|
1786
|
+
const out = [];
|
|
1787
|
+
flattenInline(nodes, fmt, theme, out);
|
|
1788
|
+
return out;
|
|
1789
|
+
},
|
|
1790
|
+
inlineText(nodes, opts = {}) {
|
|
1791
|
+
const fmt = baseFormat(theme, {
|
|
1792
|
+
...opts.size != null ? { size: opts.size } : null,
|
|
1793
|
+
...opts.weight != null ? { weight: opts.weight } : null,
|
|
1794
|
+
...opts.color != null ? { color: opts.color } : null
|
|
1795
|
+
});
|
|
1796
|
+
const out = [];
|
|
1797
|
+
flattenInline(nodes, fmt, theme, out);
|
|
1798
|
+
return (0, react.createElement)(RichText, {
|
|
1799
|
+
runs: out,
|
|
1800
|
+
font: composeFont(fmt),
|
|
1801
|
+
lineHeight: opts.lineHeight ?? theme.lineHeight,
|
|
1802
|
+
...opts.color != null ? { color: opts.color } : null,
|
|
1803
|
+
...opts.align != null ? { align: opts.align } : null
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
return ctx;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Parse `source` and render it into a tree of mugen primitives. The result is
|
|
1811
|
+
* pure and hook-free, so it can be returned straight from a `<MugenVList>`
|
|
1812
|
+
* `render` and measured by the walker. Memoization of parsing/theme/components
|
|
1813
|
+
* keeps the measure and render passes cheap.
|
|
1814
|
+
*/
|
|
1815
|
+
function renderMarkdown(source, options = {}) {
|
|
1816
|
+
const ast = parseMarkdown(source, options.parserOptions);
|
|
1817
|
+
return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
|
|
1818
|
+
}
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/markdown.tsx
|
|
1821
|
+
/**
|
|
1822
|
+
* Render markdown as a tree of mugen primitives.
|
|
1823
|
+
*
|
|
1824
|
+
* `<Markdown>` is a **pure, hook-free** component: it parses `source` with
|
|
1825
|
+
* incremark and maps the AST to mugen primitives, producing the identical tree
|
|
1826
|
+
* in mugen's measure walk and in React's render. Drop it straight into a
|
|
1827
|
+
* `<MugenVList>` `render` and every row — on- or off-screen — gets an exact,
|
|
1828
|
+
* analytic height.
|
|
1829
|
+
*
|
|
1830
|
+
* ```tsx
|
|
1831
|
+
* <MugenVList
|
|
1832
|
+
* instance={list}
|
|
1833
|
+
* getKey={(m) => m.id}
|
|
1834
|
+
* render={(m) => <Markdown source={m.body} />}
|
|
1835
|
+
* />
|
|
1836
|
+
* ```
|
|
1837
|
+
*
|
|
1838
|
+
* Extend it with typed, per-node `components` (built from the same primitives)
|
|
1839
|
+
* and a deep-partial `theme`.
|
|
1840
|
+
*/
|
|
1841
|
+
function Markdown(props) {
|
|
1842
|
+
return renderMarkdown(props.source, props);
|
|
1843
|
+
}
|
|
1844
|
+
Markdown.displayName = "Markdown";
|
|
1845
|
+
//#endregion
|
|
1846
|
+
//#region src/types.ts
|
|
1847
|
+
/**
|
|
1848
|
+
* Identity helper for authoring a typed component set with full inference and
|
|
1849
|
+
* `node` narrowing per key:
|
|
1850
|
+
*
|
|
1851
|
+
* ```tsx
|
|
1852
|
+
* const components = defineMarkdownComponents({
|
|
1853
|
+
* heading: ({ node, children }) => // node is Heading, node.depth is 1..6
|
|
1854
|
+
* <VStack>{children}</VStack>,
|
|
1855
|
+
* });
|
|
1856
|
+
* ```
|
|
1857
|
+
*/
|
|
1858
|
+
function defineMarkdownComponents(components) {
|
|
1859
|
+
return components;
|
|
1860
|
+
}
|
|
1861
|
+
//#endregion
|
|
1862
|
+
exports.CodeBlock = CodeBlock;
|
|
1863
|
+
Object.defineProperty(exports, "HStack", {
|
|
1864
|
+
enumerable: true,
|
|
1865
|
+
get: function() {
|
|
1866
|
+
return _wingleeio_mugen.HStack;
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
exports.Markdown = Markdown;
|
|
1870
|
+
exports.RichText = RichText;
|
|
1871
|
+
exports.TableBlock = TableBlock;
|
|
1872
|
+
Object.defineProperty(exports, "Text", {
|
|
1873
|
+
enumerable: true,
|
|
1874
|
+
get: function() {
|
|
1875
|
+
return _wingleeio_mugen.Text;
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
Object.defineProperty(exports, "VStack", {
|
|
1879
|
+
enumerable: true,
|
|
1880
|
+
get: function() {
|
|
1881
|
+
return _wingleeio_mugen.VStack;
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
exports.baseFormat = baseFormat;
|
|
1885
|
+
exports.clearParseCache = clearParseCache;
|
|
1886
|
+
exports.clearRichTextCache = clearRichTextCache;
|
|
1887
|
+
exports.composeFont = composeFont;
|
|
1888
|
+
exports.defaultComponents = defaultComponents;
|
|
1889
|
+
exports.defaultTheme = defaultTheme;
|
|
1890
|
+
exports.defaultTokenColors = defaultTokenColors;
|
|
1891
|
+
exports.defineMarkdownComponents = defineMarkdownComponents;
|
|
1892
|
+
Object.defineProperty(exports, "definePrimitive", {
|
|
1893
|
+
enumerable: true,
|
|
1894
|
+
get: function() {
|
|
1895
|
+
return _wingleeio_mugen.definePrimitive;
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
exports.flattenInline = flattenInline;
|
|
1899
|
+
exports.parseMarkdown = parseMarkdown;
|
|
1900
|
+
exports.profileFor = profileFor;
|
|
1901
|
+
exports.registerLanguage = registerLanguage;
|
|
1902
|
+
exports.renderMarkdown = renderMarkdown;
|
|
1903
|
+
exports.resolveTheme = resolveTheme;
|