@wingleeio/mugen-markdown 0.2.0 → 0.4.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Fragment, createElement, useEffect, useLayoutEffect, useRef, useState } 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";
2
+ import { HStack, HStack as HStack$1, Text, VStack, VStack as VStack$1, assertMeasurableFont, definePrimitive, definePrimitive as definePrimitive$1, fontEpoch, fontLonghands, fontWithLineHeight, markPrimitive, measureChildren, naturalWidthOf } from "@wingleeio/mugen";
3
3
  import { createIncremarkParser } from "@incremark/core";
4
4
  import { measureRichInlineStats, prepareRichInline } from "@chenglou/pretext/rich-inline";
5
5
  //#region src/parse.ts
@@ -226,120 +226,6 @@ function resolveTheme(theme) {
226
226
  return resolved;
227
227
  }
228
228
  //#endregion
229
- //#region src/inline.ts
230
- /** A base format for body text at a given size/weight/colour. */
231
- function baseFormat(theme, opts = {}) {
232
- return {
233
- family: theme.fontFamily,
234
- monoFamily: theme.monoFamily,
235
- size: opts.size ?? theme.fontSize,
236
- weight: opts.weight ?? 400,
237
- italic: false,
238
- mono: false,
239
- underline: false,
240
- strike: false,
241
- color: opts.color ?? (theme.color !== "inherit" ? theme.color : void 0)
242
- };
243
- }
244
- /** Compose a measurable `Font` shorthand from a format. */
245
- function composeFont(fmt) {
246
- const family = fmt.mono ? fmt.monoFamily : fmt.family;
247
- return `${fmt.italic ? "italic " : ""}${fmt.weight} ${fmt.size}px ${family}`;
248
- }
249
- function pushRun(out, text, fmt) {
250
- if (text.length === 0) return;
251
- const run = {
252
- text,
253
- font: composeFont(fmt)
254
- };
255
- if (fmt.color != null) run.color = fmt.color;
256
- if (fmt.background != null) run.background = fmt.background;
257
- const decoration = [fmt.underline ? "underline" : "", fmt.strike ? "line-through" : ""].filter(Boolean).join(" ");
258
- if (decoration) run.decoration = decoration;
259
- if (fmt.href != null) {
260
- run.href = fmt.href;
261
- run.as = "a";
262
- } else if (fmt.mono) run.as = "code";
263
- out.push(run);
264
- }
265
- /**
266
- * Flatten phrasing content into styled runs. Recursive over the inline marks;
267
- * the result feeds a single `<RichText>` so the whole paragraph wraps as one
268
- * flow and measures exactly.
269
- */
270
- function flattenInline(nodes, fmt, theme, out) {
271
- for (const node of nodes) switch (node.type) {
272
- case "text":
273
- pushRun(out, node.value, fmt);
274
- break;
275
- case "strong":
276
- flattenInline(node.children, {
277
- ...fmt,
278
- weight: theme.strongWeight
279
- }, theme, out);
280
- break;
281
- case "emphasis":
282
- flattenInline(node.children, {
283
- ...fmt,
284
- italic: theme.emphasisItalic ? true : fmt.italic
285
- }, theme, out);
286
- break;
287
- case "delete":
288
- flattenInline(node.children, {
289
- ...fmt,
290
- strike: true
291
- }, theme, out);
292
- break;
293
- case "inlineCode":
294
- pushRun(out, node.value, {
295
- ...fmt,
296
- mono: true,
297
- size: Math.round(fmt.size * theme.inlineCode.sizeScale),
298
- color: theme.inlineCode.color !== "inherit" ? theme.inlineCode.color : fmt.color,
299
- background: theme.inlineCode.background
300
- });
301
- break;
302
- case "link":
303
- flattenInline(node.children, {
304
- ...fmt,
305
- href: node.url,
306
- color: theme.link.color,
307
- underline: theme.link.underline ? true : fmt.underline
308
- }, theme, out);
309
- break;
310
- case "linkReference":
311
- flattenInline(node.children, fmt, theme, out);
312
- break;
313
- case "break":
314
- out.push({
315
- text: "",
316
- break: true
317
- });
318
- break;
319
- case "image":
320
- if (node.alt) pushRun(out, node.alt, {
321
- ...fmt,
322
- color: theme.image.color
323
- });
324
- break;
325
- case "imageReference":
326
- if (node.alt) pushRun(out, node.alt, {
327
- ...fmt,
328
- color: theme.image.color
329
- });
330
- break;
331
- case "footnoteReference":
332
- pushRun(out, `[${node.label ?? node.identifier}]`, {
333
- ...fmt,
334
- color: theme.link.color
335
- });
336
- break;
337
- default:
338
- if ("children" in node && Array.isArray(node.children)) flattenInline(node.children, fmt, theme, out);
339
- break;
340
- }
341
- }
342
- //#endregion
343
229
  //#region src/primitives/rich-text.tsx
344
230
  function resolveRunFont(run, fallback) {
345
231
  const font = run.font ?? fallback;
@@ -347,6 +233,7 @@ function resolveRunFont(run, fallback) {
347
233
  assertMeasurableFont(font);
348
234
  return font;
349
235
  }
236
+ const BOX_PLACEHOLDER = "‌";
350
237
  /** Split runs into hard-break-delimited segments, each a list of rich-inline items. */
351
238
  function segmentItems(runs, fallback) {
352
239
  const segments = [];
@@ -357,9 +244,19 @@ function segmentItems(runs, fallback) {
357
244
  cur = [];
358
245
  continue;
359
246
  }
360
- if (run.text.length === 0) continue;
247
+ if (run.advance != null) {
248
+ cur.push({
249
+ text: BOX_PLACEHOLDER,
250
+ font: resolveRunFont(run, fallback),
251
+ extraWidth: Math.max(0, run.advance),
252
+ break: "never"
253
+ });
254
+ continue;
255
+ }
256
+ const text = run.text ?? "";
257
+ if (text.length === 0) continue;
361
258
  const item = {
362
- text: run.text,
259
+ text,
363
260
  font: resolveRunFont(run, fallback)
364
261
  };
365
262
  if (run.letterSpacing != null) item.letterSpacing = run.letterSpacing;
@@ -424,6 +321,15 @@ function renderRichText(props) {
424
321
  };
425
322
  const children = props.runs.map((run, i) => {
426
323
  if (run.break) return createElement("br", { key: i });
324
+ if (run.advance != null) return createElement("span", {
325
+ key: i,
326
+ style: {
327
+ display: "inline-block",
328
+ verticalAlign: "middle",
329
+ lineHeight: 0,
330
+ whiteSpace: "nowrap"
331
+ }
332
+ }, run.content);
427
333
  const tag = run.as ?? (run.href != null ? "a" : "span");
428
334
  const elementProps = {
429
335
  key: i,
@@ -442,7 +348,7 @@ function renderRichText(props) {
442
348
  if (run.title != null) elementProps.title = run.title;
443
349
  if (run.onClick != null) elementProps.onClick = run.onClick;
444
350
  if (run.className != null) elementProps.className = run.className;
445
- return createElement(tag, elementProps, run.text);
351
+ return createElement(tag, elementProps, run.text ?? "");
446
352
  });
447
353
  return createElement("div", {
448
354
  className: props.className,
@@ -470,12 +376,173 @@ const RichText = markPrimitive(renderRichText, {
470
376
  return max;
471
377
  }
472
378
  });
379
+ /**
380
+ * Measure a string's rendered advance in px for a given measurable font — the
381
+ * same ruler `RichText` measures with. Use it to size an inline box: a text
382
+ * "pill" reserves `measureInline(label, font) + horizontalPadding`.
383
+ */
384
+ function measureInline(text, font) {
385
+ if (text.length === 0) return 0;
386
+ assertMeasurableFont(font);
387
+ return measureRichInlineStats(prepareCached([{
388
+ text,
389
+ font
390
+ }]), 1e7).maxLineWidth;
391
+ }
473
392
  /** Drop the rich-inline prepare cache (tests / memory pressure). */
474
393
  function clearRichTextCache() {
475
394
  prepCache.clear();
476
395
  cacheEpoch = -1;
477
396
  }
478
397
  //#endregion
398
+ //#region src/inline.ts
399
+ /** A base format for body text at a given size/weight/colour. */
400
+ function baseFormat(theme, opts = {}) {
401
+ return {
402
+ family: theme.fontFamily,
403
+ monoFamily: theme.monoFamily,
404
+ size: opts.size ?? theme.fontSize,
405
+ weight: opts.weight ?? 400,
406
+ italic: false,
407
+ mono: false,
408
+ underline: false,
409
+ strike: false,
410
+ color: opts.color ?? (theme.color !== "inherit" ? theme.color : void 0)
411
+ };
412
+ }
413
+ /** Compose a measurable `Font` shorthand from a format. */
414
+ function composeFont(fmt) {
415
+ const family = fmt.mono ? fmt.monoFamily : fmt.family;
416
+ return `${fmt.italic ? "italic " : ""}${fmt.weight} ${fmt.size}px ${family}`;
417
+ }
418
+ function pushRun(out, text, fmt) {
419
+ if (text.length === 0) return;
420
+ const run = {
421
+ text,
422
+ font: composeFont(fmt)
423
+ };
424
+ if (fmt.color != null) run.color = fmt.color;
425
+ if (fmt.background != null) run.background = fmt.background;
426
+ const decoration = [fmt.underline ? "underline" : "", fmt.strike ? "line-through" : ""].filter(Boolean).join(" ");
427
+ if (decoration) run.decoration = decoration;
428
+ if (fmt.href != null) {
429
+ run.href = fmt.href;
430
+ run.as = "a";
431
+ } else if (fmt.mono) run.as = "code";
432
+ out.push(run);
433
+ }
434
+ /** Build the context handed to an inline override. */
435
+ function makeInlineCtx(fmt, theme, inline) {
436
+ return {
437
+ theme,
438
+ fmt,
439
+ font: (overrides) => composeFont(overrides ? {
440
+ ...fmt,
441
+ ...overrides
442
+ } : fmt),
443
+ measure: (text, font) => measureInline(text, font),
444
+ runs: (nodes, fmtOverrides) => {
445
+ const sub = [];
446
+ flattenInline(nodes, fmtOverrides ? {
447
+ ...fmt,
448
+ ...fmtOverrides
449
+ } : fmt, theme, sub, inline);
450
+ return sub;
451
+ }
452
+ };
453
+ }
454
+ /**
455
+ * Flatten phrasing content into styled runs. Recursive over the inline marks;
456
+ * the result feeds a single `<RichText>` so the whole paragraph wraps as one
457
+ * flow and measures exactly. An `inline` override map can replace how any node
458
+ * type flattens — returning its own runs (e.g. a measured inline box) or `null`
459
+ * to fall through to the default.
460
+ */
461
+ function flattenInline(nodes, fmt, theme, out, inline) {
462
+ for (const node of nodes) {
463
+ if (inline != null) {
464
+ const override = inline[node.type];
465
+ if (override != null) {
466
+ const produced = override(node, makeInlineCtx(fmt, theme, inline));
467
+ if (produced != null) {
468
+ for (const run of produced) out.push(run);
469
+ continue;
470
+ }
471
+ }
472
+ }
473
+ switch (node.type) {
474
+ case "text":
475
+ pushRun(out, node.value, fmt);
476
+ break;
477
+ case "strong":
478
+ flattenInline(node.children, {
479
+ ...fmt,
480
+ weight: theme.strongWeight
481
+ }, theme, out, inline);
482
+ break;
483
+ case "emphasis":
484
+ flattenInline(node.children, {
485
+ ...fmt,
486
+ italic: theme.emphasisItalic ? true : fmt.italic
487
+ }, theme, out, inline);
488
+ break;
489
+ case "delete":
490
+ flattenInline(node.children, {
491
+ ...fmt,
492
+ strike: true
493
+ }, theme, out, inline);
494
+ break;
495
+ case "inlineCode":
496
+ pushRun(out, node.value, {
497
+ ...fmt,
498
+ mono: true,
499
+ size: Math.round(fmt.size * theme.inlineCode.sizeScale),
500
+ color: theme.inlineCode.color !== "inherit" ? theme.inlineCode.color : fmt.color,
501
+ background: theme.inlineCode.background
502
+ });
503
+ break;
504
+ case "link":
505
+ flattenInline(node.children, {
506
+ ...fmt,
507
+ href: node.url,
508
+ color: theme.link.color,
509
+ underline: theme.link.underline ? true : fmt.underline
510
+ }, theme, out, inline);
511
+ break;
512
+ case "linkReference":
513
+ flattenInline(node.children, fmt, theme, out, inline);
514
+ break;
515
+ case "break":
516
+ out.push({
517
+ text: "",
518
+ break: true
519
+ });
520
+ break;
521
+ case "image":
522
+ if (node.alt) pushRun(out, node.alt, {
523
+ ...fmt,
524
+ color: theme.image.color
525
+ });
526
+ break;
527
+ case "imageReference":
528
+ if (node.alt) pushRun(out, node.alt, {
529
+ ...fmt,
530
+ color: theme.image.color
531
+ });
532
+ break;
533
+ case "footnoteReference":
534
+ pushRun(out, `[${node.label ?? node.identifier}]`, {
535
+ ...fmt,
536
+ color: theme.link.color
537
+ });
538
+ break;
539
+ default:
540
+ if ("children" in node && Array.isArray(node.children)) flattenInline(node.children, fmt, theme, out, inline);
541
+ break;
542
+ }
543
+ }
544
+ }
545
+ //#endregion
479
546
  //#region src/highlight/languages.ts
480
547
  function words(s) {
481
548
  return new Set(s.split(" "));
@@ -1929,7 +1996,7 @@ function createContext(theme, components) {
1929
1996
  ...base
1930
1997
  };
1931
1998
  const out = [];
1932
- flattenInline(nodes, fmt, theme, out);
1999
+ flattenInline(nodes, fmt, theme, out, components.inline);
1933
2000
  return out;
1934
2001
  },
1935
2002
  inlineText(nodes, opts = {}) {
@@ -1939,7 +2006,7 @@ function createContext(theme, components) {
1939
2006
  ...opts.color != null ? { color: opts.color } : null
1940
2007
  });
1941
2008
  const out = [];
1942
- flattenInline(nodes, fmt, theme, out);
2009
+ flattenInline(nodes, fmt, theme, out, components.inline);
1943
2010
  return createElement(RichText, {
1944
2011
  runs: out,
1945
2012
  font: composeFont(fmt),
@@ -1962,6 +2029,256 @@ function renderMarkdown(source, options = {}) {
1962
2029
  return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
1963
2030
  }
1964
2031
  //#endregion
2032
+ //#region src/primitives/fade.tsx
2033
+ /**
2034
+ * Streaming fade-in for `<Markdown fade>`.
2035
+ *
2036
+ * The same trick the code highlighter uses, pointed at motion instead of
2037
+ * colour: the markdown DOM commits and lays out instantly (so heights,
2038
+ * selection, and stick-to-bottom stay honest), and a background-coloured veil
2039
+ * is painted over just-arrived characters and dissolved — which *reads* as the
2040
+ * text fading in. Nothing about the row ever animates; layout is done the
2041
+ * moment the text lands.
2042
+ *
2043
+ * Unlike a list-level overlay, the veil canvas lives **inside** the markdown's
2044
+ * own box (`position: absolute; inset: 0`), so it scrolls with the content and
2045
+ * needs no viewport-geometry tracking. The painter idles (no rAF) until a
2046
+ * DOM mutation arrives, so leaving `fade` on for a settled block costs nothing.
2047
+ */
2048
+ const EMA_SEED_MS = 160;
2049
+ const MIN_FADE_MS = 120;
2050
+ const MAX_FADE_MS = 400;
2051
+ const MAX_VEILS = 32;
2052
+ function commonPrefixLength(a, b) {
2053
+ const n = Math.min(a.length, b.length);
2054
+ let i = 0;
2055
+ while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
2056
+ return i;
2057
+ }
2058
+ function inChrome(node, container) {
2059
+ for (let p = node.parentElement; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
2060
+ return false;
2061
+ }
2062
+ function contentTextFilter(container) {
2063
+ return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
2064
+ }
2065
+ /** `container.textContent`, minus interactive chrome (see {@link inChrome}). */
2066
+ function contentText(container) {
2067
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, contentTextFilter(container));
2068
+ let s = "";
2069
+ for (let n = walker.nextNode(); n != null; n = walker.nextNode()) s += n.data;
2070
+ return s;
2071
+ }
2072
+ let scratch;
2073
+ function scratchCtx() {
2074
+ if (scratch === void 0) {
2075
+ const c = typeof document === "undefined" ? null : document.createElement("canvas");
2076
+ if (c == null) return scratch = null;
2077
+ c.width = 1;
2078
+ c.height = 1;
2079
+ scratch = c.getContext("2d", { willReadFrequently: true });
2080
+ }
2081
+ return scratch;
2082
+ }
2083
+ /** The opaque colour behind `el`: every ancestor's backgroundColor composited. */
2084
+ function effectiveBackground(el, cache) {
2085
+ const cached = cache.get(el);
2086
+ if (cached !== void 0) return cached;
2087
+ const layers = [];
2088
+ for (let cur = el; cur != null; cur = cur.parentElement) {
2089
+ const bg = getComputedStyle(cur).backgroundColor;
2090
+ if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") layers.push(bg);
2091
+ }
2092
+ const s = scratchCtx();
2093
+ if (s == null) return "#808080";
2094
+ s.globalAlpha = 1;
2095
+ s.fillStyle = "#ffffff";
2096
+ s.fillRect(0, 0, 1, 1);
2097
+ for (let i = layers.length - 1; i >= 0; i--) {
2098
+ try {
2099
+ s.fillStyle = layers[i];
2100
+ } catch {
2101
+ continue;
2102
+ }
2103
+ s.fillRect(0, 0, 1, 1);
2104
+ }
2105
+ const d = s.getImageData(0, 0, 1, 1).data;
2106
+ const css = `rgb(${d[0]}, ${d[1]}, ${d[2]})`;
2107
+ cache.set(el, css);
2108
+ return css;
2109
+ }
2110
+ /**
2111
+ * Paints the dissolving veil over a single content element's newly-arrived text.
2112
+ * Driven by a MutationObserver: a rAF loop runs only while veils are alive, then
2113
+ * stops until the next mutation, so an idle (settled) block uses no frames.
2114
+ */
2115
+ var FadePainter = class {
2116
+ content = null;
2117
+ canvas = null;
2118
+ ctx = null;
2119
+ prevText = "";
2120
+ veils = [];
2121
+ ema = EMA_SEED_MS;
2122
+ lastAppend = 0;
2123
+ raf = 0;
2124
+ running = false;
2125
+ mo = null;
2126
+ attach(content, canvas) {
2127
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
2128
+ const ctx = canvas.getContext("2d");
2129
+ if (ctx == null) return;
2130
+ this.content = content;
2131
+ this.canvas = canvas;
2132
+ this.ctx = ctx;
2133
+ this.prevText = contentText(content);
2134
+ this.mo = new MutationObserver(() => this.wake());
2135
+ this.mo.observe(content, {
2136
+ subtree: true,
2137
+ childList: true,
2138
+ characterData: true
2139
+ });
2140
+ }
2141
+ destroy() {
2142
+ this.mo?.disconnect();
2143
+ this.mo = null;
2144
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
2145
+ this.raf = 0;
2146
+ this.running = false;
2147
+ this.content = null;
2148
+ this.canvas = null;
2149
+ this.ctx = null;
2150
+ }
2151
+ wake() {
2152
+ if (this.running || this.content == null) return;
2153
+ this.running = true;
2154
+ this.raf = requestAnimationFrame(this.frame);
2155
+ }
2156
+ frame = () => {
2157
+ const content = this.content;
2158
+ const canvas = this.canvas;
2159
+ const ctx = this.ctx;
2160
+ if (content == null || canvas == null || ctx == null) {
2161
+ this.running = false;
2162
+ return;
2163
+ }
2164
+ const now = performance.now();
2165
+ const text = contentText(content);
2166
+ if (text !== this.prevText) {
2167
+ const prefix = commonPrefixLength(this.prevText, text);
2168
+ this.veils = this.veils.map((v) => ({
2169
+ ...v,
2170
+ start: Math.min(v.start, prefix),
2171
+ end: Math.min(v.end, prefix)
2172
+ })).filter((v) => v.end > v.start);
2173
+ if (text.length > prefix) {
2174
+ if (this.lastAppend > 0) this.ema = this.ema * .7 + Math.min(now - this.lastAppend, 1e3) * .3;
2175
+ this.lastAppend = now;
2176
+ this.veils.push({
2177
+ start: prefix,
2178
+ end: text.length,
2179
+ t0: now
2180
+ });
2181
+ if (this.veils.length > MAX_VEILS) this.veils.splice(0, this.veils.length - MAX_VEILS);
2182
+ }
2183
+ this.prevText = text;
2184
+ }
2185
+ const duration = Math.min(MAX_FADE_MS, Math.max(MIN_FADE_MS, this.ema * 3));
2186
+ const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
2187
+ this.veils = this.veils.filter((v) => (now - v.t0) * boost < duration);
2188
+ const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
2189
+ const w = content.clientWidth;
2190
+ const h = content.clientHeight;
2191
+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
2192
+ canvas.width = Math.round(w * dpr);
2193
+ canvas.height = Math.round(h * dpr);
2194
+ }
2195
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2196
+ ctx.clearRect(0, 0, w, h);
2197
+ if (this.veils.length > 0) {
2198
+ const origin = canvas.getBoundingClientRect();
2199
+ const bgCache = /* @__PURE__ */ new Map();
2200
+ const groups = /* @__PURE__ */ new Map();
2201
+ const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
2202
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
2203
+ let base = 0;
2204
+ let node = walker.nextNode();
2205
+ while (node != null) {
2206
+ const len = node.data.length;
2207
+ if (len > 0 && base + len > minStart) for (let vi = 0; vi < this.veils.length; vi++) {
2208
+ const v = this.veils[vi];
2209
+ const s = Math.max(v.start - base, 0);
2210
+ const e = Math.min(v.end - base, len);
2211
+ if (e <= s) continue;
2212
+ const parent = node.parentElement;
2213
+ if (parent == null) continue;
2214
+ const bg = effectiveBackground(parent, bgCache);
2215
+ const key = `${vi}|${bg}`;
2216
+ let group = groups.get(key);
2217
+ if (group === void 0) {
2218
+ const p = Math.min(1, (now - v.t0) * boost / duration);
2219
+ group = {
2220
+ alpha: Math.pow(1 - p, 1.6),
2221
+ bg,
2222
+ path: new Path2D()
2223
+ };
2224
+ groups.set(key, group);
2225
+ }
2226
+ const range = document.createRange();
2227
+ range.setStart(node, s);
2228
+ range.setEnd(node, e);
2229
+ for (const r of range.getClientRects()) group.path.rect(r.left - origin.left - 1, r.top - origin.top - 1, r.width + 2, r.height + 2);
2230
+ }
2231
+ base += len;
2232
+ node = walker.nextNode();
2233
+ }
2234
+ for (const group of groups.values()) {
2235
+ ctx.globalAlpha = group.alpha;
2236
+ ctx.fillStyle = group.bg;
2237
+ ctx.fill(group.path);
2238
+ }
2239
+ ctx.globalAlpha = 1;
2240
+ }
2241
+ if (this.veils.length > 0) this.raf = requestAnimationFrame(this.frame);
2242
+ else {
2243
+ this.running = false;
2244
+ this.raf = 0;
2245
+ }
2246
+ };
2247
+ };
2248
+ function renderFadeMarkdown(props) {
2249
+ const contentRef = useRef(null);
2250
+ const canvasRef = useRef(null);
2251
+ useEffect(() => {
2252
+ const content = contentRef.current;
2253
+ const canvas = canvasRef.current;
2254
+ if (content == null || canvas == null) return;
2255
+ const painter = new FadePainter();
2256
+ painter.attach(content, canvas);
2257
+ return () => painter.destroy();
2258
+ }, []);
2259
+ return createElement("div", { style: { position: "relative" } }, createElement("div", { ref: contentRef }, props.children), createElement("canvas", {
2260
+ ref: canvasRef,
2261
+ "aria-hidden": true,
2262
+ style: {
2263
+ position: "absolute",
2264
+ inset: 0,
2265
+ width: "100%",
2266
+ height: "100%",
2267
+ pointerEvents: "none"
2268
+ }
2269
+ }));
2270
+ }
2271
+ /**
2272
+ * Wraps a measured markdown subtree with a streaming fade-in canvas. Its height
2273
+ * is exactly the subtree's height — the canvas overlay is out of flow and never
2274
+ * measured.
2275
+ */
2276
+ const FadeMarkdown = markPrimitive(renderFadeMarkdown, {
2277
+ name: "FadeMarkdown",
2278
+ measure: (props, ctx) => measureChildren(props.children, ctx),
2279
+ naturalWidth: (props, ctx) => naturalWidthOf(props.children, ctx)
2280
+ });
2281
+ //#endregion
1965
2282
  //#region src/markdown.tsx
1966
2283
  /**
1967
2284
  * Render markdown as a tree of mugen primitives.
@@ -1984,7 +2301,9 @@ function renderMarkdown(source, options = {}) {
1984
2301
  * and a deep-partial `theme`.
1985
2302
  */
1986
2303
  function Markdown(props) {
1987
- return renderMarkdown(props.source, props);
2304
+ const content = renderMarkdown(props.source, props);
2305
+ if (props.fade && content != null) return createElement(FadeMarkdown, null, content);
2306
+ return content;
1988
2307
  }
1989
2308
  Markdown.displayName = "Markdown";
1990
2309
  //#endregion
@@ -2004,4 +2323,4 @@ function defineMarkdownComponents(components) {
2004
2323
  return components;
2005
2324
  }
2006
2325
  //#endregion
2007
- export { CodeBlock, HStack, Markdown, RichText, TableBlock, Text, VStack, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
2326
+ export { CodeBlock, FadeMarkdown, HStack, Markdown, RichText, TableBlock, Text, VStack, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, measureInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wingleeio/mugen-markdown",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Measurable markdown for mugen — incremark-parsed, rendered with mugen primitives so the virtualizer's tree walker computes exact row heights.",
5
5
  "license": "MIT",
6
6
  "author": "Wing Lee <contact@winglee.io>",