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