@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/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;