@wingleeio/mugen-markdown 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1963,6 +1963,256 @@ function renderMarkdown(source, options = {}) {
1963
1963
  return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
1964
1964
  }
1965
1965
  //#endregion
1966
+ //#region src/primitives/fade.tsx
1967
+ /**
1968
+ * Streaming fade-in for `<Markdown fade>`.
1969
+ *
1970
+ * The same trick the code highlighter uses, pointed at motion instead of
1971
+ * colour: the markdown DOM commits and lays out instantly (so heights,
1972
+ * selection, and stick-to-bottom stay honest), and a background-coloured veil
1973
+ * is painted over just-arrived characters and dissolved — which *reads* as the
1974
+ * text fading in. Nothing about the row ever animates; layout is done the
1975
+ * moment the text lands.
1976
+ *
1977
+ * Unlike a list-level overlay, the veil canvas lives **inside** the markdown's
1978
+ * own box (`position: absolute; inset: 0`), so it scrolls with the content and
1979
+ * needs no viewport-geometry tracking. The painter idles (no rAF) until a
1980
+ * DOM mutation arrives, so leaving `fade` on for a settled block costs nothing.
1981
+ */
1982
+ const EMA_SEED_MS = 160;
1983
+ const MIN_FADE_MS = 120;
1984
+ const MAX_FADE_MS = 400;
1985
+ const MAX_VEILS = 32;
1986
+ function commonPrefixLength(a, b) {
1987
+ const n = Math.min(a.length, b.length);
1988
+ let i = 0;
1989
+ while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
1990
+ return i;
1991
+ }
1992
+ function inChrome(node, container) {
1993
+ for (let p = node.parentElement; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
1994
+ return false;
1995
+ }
1996
+ function contentTextFilter(container) {
1997
+ return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
1998
+ }
1999
+ /** `container.textContent`, minus interactive chrome (see {@link inChrome}). */
2000
+ function contentText(container) {
2001
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, contentTextFilter(container));
2002
+ let s = "";
2003
+ for (let n = walker.nextNode(); n != null; n = walker.nextNode()) s += n.data;
2004
+ return s;
2005
+ }
2006
+ let scratch;
2007
+ function scratchCtx() {
2008
+ if (scratch === void 0) {
2009
+ const c = typeof document === "undefined" ? null : document.createElement("canvas");
2010
+ if (c == null) return scratch = null;
2011
+ c.width = 1;
2012
+ c.height = 1;
2013
+ scratch = c.getContext("2d", { willReadFrequently: true });
2014
+ }
2015
+ return scratch;
2016
+ }
2017
+ /** The opaque colour behind `el`: every ancestor's backgroundColor composited. */
2018
+ function effectiveBackground(el, cache) {
2019
+ const cached = cache.get(el);
2020
+ if (cached !== void 0) return cached;
2021
+ const layers = [];
2022
+ for (let cur = el; cur != null; cur = cur.parentElement) {
2023
+ const bg = getComputedStyle(cur).backgroundColor;
2024
+ if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") layers.push(bg);
2025
+ }
2026
+ const s = scratchCtx();
2027
+ if (s == null) return "#808080";
2028
+ s.globalAlpha = 1;
2029
+ s.fillStyle = "#ffffff";
2030
+ s.fillRect(0, 0, 1, 1);
2031
+ for (let i = layers.length - 1; i >= 0; i--) {
2032
+ try {
2033
+ s.fillStyle = layers[i];
2034
+ } catch {
2035
+ continue;
2036
+ }
2037
+ s.fillRect(0, 0, 1, 1);
2038
+ }
2039
+ const d = s.getImageData(0, 0, 1, 1).data;
2040
+ const css = `rgb(${d[0]}, ${d[1]}, ${d[2]})`;
2041
+ cache.set(el, css);
2042
+ return css;
2043
+ }
2044
+ /**
2045
+ * Paints the dissolving veil over a single content element's newly-arrived text.
2046
+ * Driven by a MutationObserver: a rAF loop runs only while veils are alive, then
2047
+ * stops until the next mutation, so an idle (settled) block uses no frames.
2048
+ */
2049
+ var FadePainter = class {
2050
+ content = null;
2051
+ canvas = null;
2052
+ ctx = null;
2053
+ prevText = "";
2054
+ veils = [];
2055
+ ema = EMA_SEED_MS;
2056
+ lastAppend = 0;
2057
+ raf = 0;
2058
+ running = false;
2059
+ mo = null;
2060
+ attach(content, canvas) {
2061
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
2062
+ const ctx = canvas.getContext("2d");
2063
+ if (ctx == null) return;
2064
+ this.content = content;
2065
+ this.canvas = canvas;
2066
+ this.ctx = ctx;
2067
+ this.prevText = contentText(content);
2068
+ this.mo = new MutationObserver(() => this.wake());
2069
+ this.mo.observe(content, {
2070
+ subtree: true,
2071
+ childList: true,
2072
+ characterData: true
2073
+ });
2074
+ }
2075
+ destroy() {
2076
+ this.mo?.disconnect();
2077
+ this.mo = null;
2078
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
2079
+ this.raf = 0;
2080
+ this.running = false;
2081
+ this.content = null;
2082
+ this.canvas = null;
2083
+ this.ctx = null;
2084
+ }
2085
+ wake() {
2086
+ if (this.running || this.content == null) return;
2087
+ this.running = true;
2088
+ this.raf = requestAnimationFrame(this.frame);
2089
+ }
2090
+ frame = () => {
2091
+ const content = this.content;
2092
+ const canvas = this.canvas;
2093
+ const ctx = this.ctx;
2094
+ if (content == null || canvas == null || ctx == null) {
2095
+ this.running = false;
2096
+ return;
2097
+ }
2098
+ const now = performance.now();
2099
+ const text = contentText(content);
2100
+ if (text !== this.prevText) {
2101
+ const prefix = commonPrefixLength(this.prevText, text);
2102
+ this.veils = this.veils.map((v) => ({
2103
+ ...v,
2104
+ start: Math.min(v.start, prefix),
2105
+ end: Math.min(v.end, prefix)
2106
+ })).filter((v) => v.end > v.start);
2107
+ if (text.length > prefix) {
2108
+ if (this.lastAppend > 0) this.ema = this.ema * .7 + Math.min(now - this.lastAppend, 1e3) * .3;
2109
+ this.lastAppend = now;
2110
+ this.veils.push({
2111
+ start: prefix,
2112
+ end: text.length,
2113
+ t0: now
2114
+ });
2115
+ if (this.veils.length > MAX_VEILS) this.veils.splice(0, this.veils.length - MAX_VEILS);
2116
+ }
2117
+ this.prevText = text;
2118
+ }
2119
+ const duration = Math.min(MAX_FADE_MS, Math.max(MIN_FADE_MS, this.ema * 3));
2120
+ const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
2121
+ this.veils = this.veils.filter((v) => (now - v.t0) * boost < duration);
2122
+ const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
2123
+ const w = content.clientWidth;
2124
+ const h = content.clientHeight;
2125
+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
2126
+ canvas.width = Math.round(w * dpr);
2127
+ canvas.height = Math.round(h * dpr);
2128
+ }
2129
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2130
+ ctx.clearRect(0, 0, w, h);
2131
+ if (this.veils.length > 0) {
2132
+ const origin = canvas.getBoundingClientRect();
2133
+ const bgCache = /* @__PURE__ */ new Map();
2134
+ const groups = /* @__PURE__ */ new Map();
2135
+ const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
2136
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
2137
+ let base = 0;
2138
+ let node = walker.nextNode();
2139
+ while (node != null) {
2140
+ const len = node.data.length;
2141
+ if (len > 0 && base + len > minStart) for (let vi = 0; vi < this.veils.length; vi++) {
2142
+ const v = this.veils[vi];
2143
+ const s = Math.max(v.start - base, 0);
2144
+ const e = Math.min(v.end - base, len);
2145
+ if (e <= s) continue;
2146
+ const parent = node.parentElement;
2147
+ if (parent == null) continue;
2148
+ const bg = effectiveBackground(parent, bgCache);
2149
+ const key = `${vi}|${bg}`;
2150
+ let group = groups.get(key);
2151
+ if (group === void 0) {
2152
+ const p = Math.min(1, (now - v.t0) * boost / duration);
2153
+ group = {
2154
+ alpha: Math.pow(1 - p, 1.6),
2155
+ bg,
2156
+ path: new Path2D()
2157
+ };
2158
+ groups.set(key, group);
2159
+ }
2160
+ const range = document.createRange();
2161
+ range.setStart(node, s);
2162
+ range.setEnd(node, e);
2163
+ 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);
2164
+ }
2165
+ base += len;
2166
+ node = walker.nextNode();
2167
+ }
2168
+ for (const group of groups.values()) {
2169
+ ctx.globalAlpha = group.alpha;
2170
+ ctx.fillStyle = group.bg;
2171
+ ctx.fill(group.path);
2172
+ }
2173
+ ctx.globalAlpha = 1;
2174
+ }
2175
+ if (this.veils.length > 0) this.raf = requestAnimationFrame(this.frame);
2176
+ else {
2177
+ this.running = false;
2178
+ this.raf = 0;
2179
+ }
2180
+ };
2181
+ };
2182
+ function renderFadeMarkdown(props) {
2183
+ const contentRef = (0, react.useRef)(null);
2184
+ const canvasRef = (0, react.useRef)(null);
2185
+ (0, react.useEffect)(() => {
2186
+ const content = contentRef.current;
2187
+ const canvas = canvasRef.current;
2188
+ if (content == null || canvas == null) return;
2189
+ const painter = new FadePainter();
2190
+ painter.attach(content, canvas);
2191
+ return () => painter.destroy();
2192
+ }, []);
2193
+ return (0, react.createElement)("div", { style: { position: "relative" } }, (0, react.createElement)("div", { ref: contentRef }, props.children), (0, react.createElement)("canvas", {
2194
+ ref: canvasRef,
2195
+ "aria-hidden": true,
2196
+ style: {
2197
+ position: "absolute",
2198
+ inset: 0,
2199
+ width: "100%",
2200
+ height: "100%",
2201
+ pointerEvents: "none"
2202
+ }
2203
+ }));
2204
+ }
2205
+ /**
2206
+ * Wraps a measured markdown subtree with a streaming fade-in canvas. Its height
2207
+ * is exactly the subtree's height — the canvas overlay is out of flow and never
2208
+ * measured.
2209
+ */
2210
+ const FadeMarkdown = (0, _wingleeio_mugen.markPrimitive)(renderFadeMarkdown, {
2211
+ name: "FadeMarkdown",
2212
+ measure: (props, ctx) => (0, _wingleeio_mugen.measureChildren)(props.children, ctx),
2213
+ naturalWidth: (props, ctx) => (0, _wingleeio_mugen.naturalWidthOf)(props.children, ctx)
2214
+ });
2215
+ //#endregion
1966
2216
  //#region src/markdown.tsx
1967
2217
  /**
1968
2218
  * Render markdown as a tree of mugen primitives.
@@ -1985,7 +2235,9 @@ function renderMarkdown(source, options = {}) {
1985
2235
  * and a deep-partial `theme`.
1986
2236
  */
1987
2237
  function Markdown(props) {
1988
- return renderMarkdown(props.source, props);
2238
+ const content = renderMarkdown(props.source, props);
2239
+ if (props.fade && content != null) return (0, react.createElement)(FadeMarkdown, null, content);
2240
+ return content;
1989
2241
  }
1990
2242
  Markdown.displayName = "Markdown";
1991
2243
  //#endregion
@@ -2006,6 +2258,7 @@ function defineMarkdownComponents(components) {
2006
2258
  }
2007
2259
  //#endregion
2008
2260
  exports.CodeBlock = CodeBlock;
2261
+ exports.FadeMarkdown = FadeMarkdown;
2009
2262
  Object.defineProperty(exports, "HStack", {
2010
2263
  enumerable: true,
2011
2264
  get: function() {
package/dist/index.d.cts CHANGED
@@ -369,6 +369,13 @@ declare function renderMarkdown(source: string, options?: RenderMarkdownOptions)
369
369
  interface MarkdownProps extends RenderMarkdownOptions {
370
370
  /** The markdown source to render. */
371
371
  source: string;
372
+ /**
373
+ * Fade just-arrived text in as the source streams. The DOM still commits and
374
+ * lays out instantly (heights stay exact); a veil over new characters
375
+ * dissolves, which reads as a fade-in. Leaving it on for a settled block is
376
+ * free. Honours `prefers-reduced-motion`.
377
+ */
378
+ fade?: boolean;
372
379
  }
373
380
  /**
374
381
  * Render markdown as a tree of mugen primitives.
@@ -496,6 +503,18 @@ interface TableBlockProps<C extends string = string> {
496
503
  /** A measurable GFM-table primitive with aligned, content-proportional columns. */
497
504
  declare const TableBlock: <C extends string = string>(props: TableBlockProps<C>) => ReactElement;
498
505
  //#endregion
506
+ //#region src/primitives/fade.d.ts
507
+ interface FadeMarkdownProps {
508
+ /** The rendered markdown subtree to veil. */
509
+ children?: ReactNode;
510
+ }
511
+ /**
512
+ * Wraps a measured markdown subtree with a streaming fade-in canvas. Its height
513
+ * is exactly the subtree's height — the canvas overlay is out of flow and never
514
+ * measured.
515
+ */
516
+ declare const FadeMarkdown: (props: FadeMarkdownProps) => ReactElement;
517
+ //#endregion
499
518
  //#region src/highlight/languages.d.ts
500
519
  /**
501
520
  * Language profiles for the built-in tokenizer. A profile is a small data
@@ -537,4 +556,4 @@ declare function registerLanguage(names: string | readonly string[], p: Language
537
556
  /** The profile for a fence language, or `null` when the language is unknown. */
538
557
  declare function profileFor(lang: string | undefined): LanguageProfile | null;
539
558
  //#endregion
540
- export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockHeader, type CodeBlockProps, type CodeTokenColors, type DeepPartial, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
559
+ export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockHeader, type CodeBlockProps, type CodeTokenColors, type DeepPartial, FadeMarkdown, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
package/dist/index.d.mts CHANGED
@@ -369,6 +369,13 @@ declare function renderMarkdown(source: string, options?: RenderMarkdownOptions)
369
369
  interface MarkdownProps extends RenderMarkdownOptions {
370
370
  /** The markdown source to render. */
371
371
  source: string;
372
+ /**
373
+ * Fade just-arrived text in as the source streams. The DOM still commits and
374
+ * lays out instantly (heights stay exact); a veil over new characters
375
+ * dissolves, which reads as a fade-in. Leaving it on for a settled block is
376
+ * free. Honours `prefers-reduced-motion`.
377
+ */
378
+ fade?: boolean;
372
379
  }
373
380
  /**
374
381
  * Render markdown as a tree of mugen primitives.
@@ -496,6 +503,18 @@ interface TableBlockProps<C extends string = string> {
496
503
  /** A measurable GFM-table primitive with aligned, content-proportional columns. */
497
504
  declare const TableBlock: <C extends string = string>(props: TableBlockProps<C>) => ReactElement;
498
505
  //#endregion
506
+ //#region src/primitives/fade.d.ts
507
+ interface FadeMarkdownProps {
508
+ /** The rendered markdown subtree to veil. */
509
+ children?: ReactNode;
510
+ }
511
+ /**
512
+ * Wraps a measured markdown subtree with a streaming fade-in canvas. Its height
513
+ * is exactly the subtree's height — the canvas overlay is out of flow and never
514
+ * measured.
515
+ */
516
+ declare const FadeMarkdown: (props: FadeMarkdownProps) => ReactElement;
517
+ //#endregion
499
518
  //#region src/highlight/languages.d.ts
500
519
  /**
501
520
  * Language profiles for the built-in tokenizer. A profile is a small data
@@ -537,4 +556,4 @@ declare function registerLanguage(names: string | readonly string[], p: Language
537
556
  /** The profile for a fence language, or `null` when the language is unknown. */
538
557
  declare function profileFor(lang: string | undefined): LanguageProfile | null;
539
558
  //#endregion
540
- export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockHeader, type CodeBlockProps, type CodeTokenColors, type DeepPartial, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
559
+ export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockHeader, type CodeBlockProps, type CodeTokenColors, type DeepPartial, FadeMarkdown, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
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
@@ -1962,6 +1962,256 @@ function renderMarkdown(source, options = {}) {
1962
1962
  return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
1963
1963
  }
1964
1964
  //#endregion
1965
+ //#region src/primitives/fade.tsx
1966
+ /**
1967
+ * Streaming fade-in for `<Markdown fade>`.
1968
+ *
1969
+ * The same trick the code highlighter uses, pointed at motion instead of
1970
+ * colour: the markdown DOM commits and lays out instantly (so heights,
1971
+ * selection, and stick-to-bottom stay honest), and a background-coloured veil
1972
+ * is painted over just-arrived characters and dissolved — which *reads* as the
1973
+ * text fading in. Nothing about the row ever animates; layout is done the
1974
+ * moment the text lands.
1975
+ *
1976
+ * Unlike a list-level overlay, the veil canvas lives **inside** the markdown's
1977
+ * own box (`position: absolute; inset: 0`), so it scrolls with the content and
1978
+ * needs no viewport-geometry tracking. The painter idles (no rAF) until a
1979
+ * DOM mutation arrives, so leaving `fade` on for a settled block costs nothing.
1980
+ */
1981
+ const EMA_SEED_MS = 160;
1982
+ const MIN_FADE_MS = 120;
1983
+ const MAX_FADE_MS = 400;
1984
+ const MAX_VEILS = 32;
1985
+ function commonPrefixLength(a, b) {
1986
+ const n = Math.min(a.length, b.length);
1987
+ let i = 0;
1988
+ while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
1989
+ return i;
1990
+ }
1991
+ function inChrome(node, container) {
1992
+ for (let p = node.parentElement; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
1993
+ return false;
1994
+ }
1995
+ function contentTextFilter(container) {
1996
+ return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
1997
+ }
1998
+ /** `container.textContent`, minus interactive chrome (see {@link inChrome}). */
1999
+ function contentText(container) {
2000
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, contentTextFilter(container));
2001
+ let s = "";
2002
+ for (let n = walker.nextNode(); n != null; n = walker.nextNode()) s += n.data;
2003
+ return s;
2004
+ }
2005
+ let scratch;
2006
+ function scratchCtx() {
2007
+ if (scratch === void 0) {
2008
+ const c = typeof document === "undefined" ? null : document.createElement("canvas");
2009
+ if (c == null) return scratch = null;
2010
+ c.width = 1;
2011
+ c.height = 1;
2012
+ scratch = c.getContext("2d", { willReadFrequently: true });
2013
+ }
2014
+ return scratch;
2015
+ }
2016
+ /** The opaque colour behind `el`: every ancestor's backgroundColor composited. */
2017
+ function effectiveBackground(el, cache) {
2018
+ const cached = cache.get(el);
2019
+ if (cached !== void 0) return cached;
2020
+ const layers = [];
2021
+ for (let cur = el; cur != null; cur = cur.parentElement) {
2022
+ const bg = getComputedStyle(cur).backgroundColor;
2023
+ if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") layers.push(bg);
2024
+ }
2025
+ const s = scratchCtx();
2026
+ if (s == null) return "#808080";
2027
+ s.globalAlpha = 1;
2028
+ s.fillStyle = "#ffffff";
2029
+ s.fillRect(0, 0, 1, 1);
2030
+ for (let i = layers.length - 1; i >= 0; i--) {
2031
+ try {
2032
+ s.fillStyle = layers[i];
2033
+ } catch {
2034
+ continue;
2035
+ }
2036
+ s.fillRect(0, 0, 1, 1);
2037
+ }
2038
+ const d = s.getImageData(0, 0, 1, 1).data;
2039
+ const css = `rgb(${d[0]}, ${d[1]}, ${d[2]})`;
2040
+ cache.set(el, css);
2041
+ return css;
2042
+ }
2043
+ /**
2044
+ * Paints the dissolving veil over a single content element's newly-arrived text.
2045
+ * Driven by a MutationObserver: a rAF loop runs only while veils are alive, then
2046
+ * stops until the next mutation, so an idle (settled) block uses no frames.
2047
+ */
2048
+ var FadePainter = class {
2049
+ content = null;
2050
+ canvas = null;
2051
+ ctx = null;
2052
+ prevText = "";
2053
+ veils = [];
2054
+ ema = EMA_SEED_MS;
2055
+ lastAppend = 0;
2056
+ raf = 0;
2057
+ running = false;
2058
+ mo = null;
2059
+ attach(content, canvas) {
2060
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
2061
+ const ctx = canvas.getContext("2d");
2062
+ if (ctx == null) return;
2063
+ this.content = content;
2064
+ this.canvas = canvas;
2065
+ this.ctx = ctx;
2066
+ this.prevText = contentText(content);
2067
+ this.mo = new MutationObserver(() => this.wake());
2068
+ this.mo.observe(content, {
2069
+ subtree: true,
2070
+ childList: true,
2071
+ characterData: true
2072
+ });
2073
+ }
2074
+ destroy() {
2075
+ this.mo?.disconnect();
2076
+ this.mo = null;
2077
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
2078
+ this.raf = 0;
2079
+ this.running = false;
2080
+ this.content = null;
2081
+ this.canvas = null;
2082
+ this.ctx = null;
2083
+ }
2084
+ wake() {
2085
+ if (this.running || this.content == null) return;
2086
+ this.running = true;
2087
+ this.raf = requestAnimationFrame(this.frame);
2088
+ }
2089
+ frame = () => {
2090
+ const content = this.content;
2091
+ const canvas = this.canvas;
2092
+ const ctx = this.ctx;
2093
+ if (content == null || canvas == null || ctx == null) {
2094
+ this.running = false;
2095
+ return;
2096
+ }
2097
+ const now = performance.now();
2098
+ const text = contentText(content);
2099
+ if (text !== this.prevText) {
2100
+ const prefix = commonPrefixLength(this.prevText, text);
2101
+ this.veils = this.veils.map((v) => ({
2102
+ ...v,
2103
+ start: Math.min(v.start, prefix),
2104
+ end: Math.min(v.end, prefix)
2105
+ })).filter((v) => v.end > v.start);
2106
+ if (text.length > prefix) {
2107
+ if (this.lastAppend > 0) this.ema = this.ema * .7 + Math.min(now - this.lastAppend, 1e3) * .3;
2108
+ this.lastAppend = now;
2109
+ this.veils.push({
2110
+ start: prefix,
2111
+ end: text.length,
2112
+ t0: now
2113
+ });
2114
+ if (this.veils.length > MAX_VEILS) this.veils.splice(0, this.veils.length - MAX_VEILS);
2115
+ }
2116
+ this.prevText = text;
2117
+ }
2118
+ const duration = Math.min(MAX_FADE_MS, Math.max(MIN_FADE_MS, this.ema * 3));
2119
+ const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
2120
+ this.veils = this.veils.filter((v) => (now - v.t0) * boost < duration);
2121
+ const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
2122
+ const w = content.clientWidth;
2123
+ const h = content.clientHeight;
2124
+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
2125
+ canvas.width = Math.round(w * dpr);
2126
+ canvas.height = Math.round(h * dpr);
2127
+ }
2128
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2129
+ ctx.clearRect(0, 0, w, h);
2130
+ if (this.veils.length > 0) {
2131
+ const origin = canvas.getBoundingClientRect();
2132
+ const bgCache = /* @__PURE__ */ new Map();
2133
+ const groups = /* @__PURE__ */ new Map();
2134
+ const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
2135
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
2136
+ let base = 0;
2137
+ let node = walker.nextNode();
2138
+ while (node != null) {
2139
+ const len = node.data.length;
2140
+ if (len > 0 && base + len > minStart) for (let vi = 0; vi < this.veils.length; vi++) {
2141
+ const v = this.veils[vi];
2142
+ const s = Math.max(v.start - base, 0);
2143
+ const e = Math.min(v.end - base, len);
2144
+ if (e <= s) continue;
2145
+ const parent = node.parentElement;
2146
+ if (parent == null) continue;
2147
+ const bg = effectiveBackground(parent, bgCache);
2148
+ const key = `${vi}|${bg}`;
2149
+ let group = groups.get(key);
2150
+ if (group === void 0) {
2151
+ const p = Math.min(1, (now - v.t0) * boost / duration);
2152
+ group = {
2153
+ alpha: Math.pow(1 - p, 1.6),
2154
+ bg,
2155
+ path: new Path2D()
2156
+ };
2157
+ groups.set(key, group);
2158
+ }
2159
+ const range = document.createRange();
2160
+ range.setStart(node, s);
2161
+ range.setEnd(node, e);
2162
+ 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);
2163
+ }
2164
+ base += len;
2165
+ node = walker.nextNode();
2166
+ }
2167
+ for (const group of groups.values()) {
2168
+ ctx.globalAlpha = group.alpha;
2169
+ ctx.fillStyle = group.bg;
2170
+ ctx.fill(group.path);
2171
+ }
2172
+ ctx.globalAlpha = 1;
2173
+ }
2174
+ if (this.veils.length > 0) this.raf = requestAnimationFrame(this.frame);
2175
+ else {
2176
+ this.running = false;
2177
+ this.raf = 0;
2178
+ }
2179
+ };
2180
+ };
2181
+ function renderFadeMarkdown(props) {
2182
+ const contentRef = useRef(null);
2183
+ const canvasRef = useRef(null);
2184
+ useEffect(() => {
2185
+ const content = contentRef.current;
2186
+ const canvas = canvasRef.current;
2187
+ if (content == null || canvas == null) return;
2188
+ const painter = new FadePainter();
2189
+ painter.attach(content, canvas);
2190
+ return () => painter.destroy();
2191
+ }, []);
2192
+ return createElement("div", { style: { position: "relative" } }, createElement("div", { ref: contentRef }, props.children), createElement("canvas", {
2193
+ ref: canvasRef,
2194
+ "aria-hidden": true,
2195
+ style: {
2196
+ position: "absolute",
2197
+ inset: 0,
2198
+ width: "100%",
2199
+ height: "100%",
2200
+ pointerEvents: "none"
2201
+ }
2202
+ }));
2203
+ }
2204
+ /**
2205
+ * Wraps a measured markdown subtree with a streaming fade-in canvas. Its height
2206
+ * is exactly the subtree's height — the canvas overlay is out of flow and never
2207
+ * measured.
2208
+ */
2209
+ const FadeMarkdown = markPrimitive(renderFadeMarkdown, {
2210
+ name: "FadeMarkdown",
2211
+ measure: (props, ctx) => measureChildren(props.children, ctx),
2212
+ naturalWidth: (props, ctx) => naturalWidthOf(props.children, ctx)
2213
+ });
2214
+ //#endregion
1965
2215
  //#region src/markdown.tsx
1966
2216
  /**
1967
2217
  * Render markdown as a tree of mugen primitives.
@@ -1984,7 +2234,9 @@ function renderMarkdown(source, options = {}) {
1984
2234
  * and a deep-partial `theme`.
1985
2235
  */
1986
2236
  function Markdown(props) {
1987
- return renderMarkdown(props.source, props);
2237
+ const content = renderMarkdown(props.source, props);
2238
+ if (props.fade && content != null) return createElement(FadeMarkdown, null, content);
2239
+ return content;
1988
2240
  }
1989
2241
  Markdown.displayName = "Markdown";
1990
2242
  //#endregion
@@ -2004,4 +2256,4 @@ function defineMarkdownComponents(components) {
2004
2256
  return components;
2005
2257
  }
2006
2258
  //#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 };
2259
+ export { CodeBlock, FadeMarkdown, HStack, Markdown, RichText, TableBlock, Text, VStack, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, 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.3.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>",