@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 +254 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.mts +20 -1
- package/dist/index.mjs +255 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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>",
|