@wingleeio/mugen-markdown 0.1.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 +412 -13
- package/dist/index.d.cts +64 -3
- package/dist/index.d.mts +64 -3
- package/dist/index.mjs +414 -16
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -162,7 +162,16 @@ const defaultTheme = {
|
|
|
162
162
|
background: "rgba(127, 127, 127, 0.12)",
|
|
163
163
|
color: "inherit",
|
|
164
164
|
radius: 8,
|
|
165
|
-
highlight: defaultTokenColors
|
|
165
|
+
highlight: defaultTokenColors,
|
|
166
|
+
header: {
|
|
167
|
+
show: false,
|
|
168
|
+
height: 38,
|
|
169
|
+
fontSize: 12,
|
|
170
|
+
background: "rgba(127, 127, 127, 0.06)",
|
|
171
|
+
color: "rgba(127, 127, 127, 0.85)",
|
|
172
|
+
borderColor: "rgba(127, 127, 127, 0.18)",
|
|
173
|
+
buttonBackground: "rgba(127, 127, 127, 0.04)"
|
|
174
|
+
}
|
|
166
175
|
},
|
|
167
176
|
blockquote: {
|
|
168
177
|
padding: 14,
|
|
@@ -1311,7 +1320,7 @@ function lineCount(value) {
|
|
|
1311
1320
|
function measureCodeBlock(props, ctx) {
|
|
1312
1321
|
(0, _wingleeio_mugen.assertMeasurableFont)(props.font);
|
|
1313
1322
|
const pad = props.padding ?? 0;
|
|
1314
|
-
return lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1323
|
+
return (props.header ? props.header.height : 0) + lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1315
1324
|
}
|
|
1316
1325
|
const colorsCache = /* @__PURE__ */ new WeakMap();
|
|
1317
1326
|
function resolveTokenColors(overrides) {
|
|
@@ -1375,9 +1384,124 @@ function HighlightedCode(props) {
|
|
|
1375
1384
|
style: overlayStyle
|
|
1376
1385
|
}));
|
|
1377
1386
|
}
|
|
1387
|
+
function legacyCopy(text) {
|
|
1388
|
+
if (typeof document === "undefined") return false;
|
|
1389
|
+
const ta = document.createElement("textarea");
|
|
1390
|
+
ta.value = text;
|
|
1391
|
+
ta.setAttribute("readonly", "");
|
|
1392
|
+
ta.style.position = "fixed";
|
|
1393
|
+
ta.style.top = "0";
|
|
1394
|
+
ta.style.left = "0";
|
|
1395
|
+
ta.style.opacity = "0";
|
|
1396
|
+
ta.style.pointerEvents = "none";
|
|
1397
|
+
document.body.appendChild(ta);
|
|
1398
|
+
ta.select();
|
|
1399
|
+
let ok = false;
|
|
1400
|
+
try {
|
|
1401
|
+
ok = document.execCommand("copy");
|
|
1402
|
+
} catch {
|
|
1403
|
+
ok = false;
|
|
1404
|
+
}
|
|
1405
|
+
document.body.removeChild(ta);
|
|
1406
|
+
return ok;
|
|
1407
|
+
}
|
|
1408
|
+
async function copyText(text) {
|
|
1409
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
1410
|
+
if (clip?.writeText != null) try {
|
|
1411
|
+
await clip.writeText(text);
|
|
1412
|
+
return true;
|
|
1413
|
+
} catch {}
|
|
1414
|
+
return legacyCopy(text);
|
|
1415
|
+
}
|
|
1416
|
+
/** Copies the block's raw text; flips to "Copied" for ~1.6s on success. */
|
|
1417
|
+
function CopyButton(props) {
|
|
1418
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1419
|
+
const [hovered, setHovered] = (0, react.useState)(false);
|
|
1420
|
+
const timer = (0, react.useRef)(null);
|
|
1421
|
+
(0, react.useEffect)(() => () => {
|
|
1422
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1423
|
+
}, []);
|
|
1424
|
+
const onClick = () => {
|
|
1425
|
+
copyText(props.value).then((ok) => {
|
|
1426
|
+
if (!ok) return;
|
|
1427
|
+
setCopied(true);
|
|
1428
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1429
|
+
timer.current = setTimeout(() => setCopied(false), 1600);
|
|
1430
|
+
}).catch(() => {});
|
|
1431
|
+
};
|
|
1432
|
+
const style = {
|
|
1433
|
+
display: "inline-flex",
|
|
1434
|
+
flex: "0 0 auto",
|
|
1435
|
+
alignItems: "center",
|
|
1436
|
+
justifyContent: "center",
|
|
1437
|
+
whiteSpace: "nowrap",
|
|
1438
|
+
cursor: "pointer",
|
|
1439
|
+
borderRadius: 8,
|
|
1440
|
+
border: `1px solid ${props.borderColor ?? "rgba(127, 127, 127, 0.2)"}`,
|
|
1441
|
+
...props.background != null ? { background: props.background } : null,
|
|
1442
|
+
...props.color != null ? { color: props.color } : null,
|
|
1443
|
+
padding: "4px 9px",
|
|
1444
|
+
minWidth: "4.6em",
|
|
1445
|
+
fontFamily: "inherit",
|
|
1446
|
+
fontSize: `${props.fontSize}px`,
|
|
1447
|
+
lineHeight: 1,
|
|
1448
|
+
opacity: hovered || copied ? 1 : .82,
|
|
1449
|
+
transition: "opacity 120ms ease"
|
|
1450
|
+
};
|
|
1451
|
+
return (0, react.createElement)("button", {
|
|
1452
|
+
type: "button",
|
|
1453
|
+
onClick,
|
|
1454
|
+
onMouseEnter: () => setHovered(true),
|
|
1455
|
+
onMouseLeave: () => setHovered(false),
|
|
1456
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
1457
|
+
style
|
|
1458
|
+
}, copied ? "Copied" : "Copy");
|
|
1459
|
+
}
|
|
1460
|
+
/** The fixed-height chrome bar: language label left, copy button right. */
|
|
1461
|
+
function CodeHeader(props) {
|
|
1462
|
+
const barStyle = {
|
|
1463
|
+
display: "flex",
|
|
1464
|
+
flex: "0 0 auto",
|
|
1465
|
+
alignItems: "center",
|
|
1466
|
+
justifyContent: "space-between",
|
|
1467
|
+
gap: 8,
|
|
1468
|
+
height: `${props.height}px`,
|
|
1469
|
+
boxSizing: "border-box",
|
|
1470
|
+
padding: "0 12px",
|
|
1471
|
+
fontFamily: props.fontFamily,
|
|
1472
|
+
...props.radius != null ? {
|
|
1473
|
+
borderTopLeftRadius: `${props.radius}px`,
|
|
1474
|
+
borderTopRightRadius: `${props.radius}px`
|
|
1475
|
+
} : null,
|
|
1476
|
+
...props.background != null ? { background: props.background } : null,
|
|
1477
|
+
...props.borderColor != null ? { borderBottom: `1px solid ${props.borderColor}` } : null
|
|
1478
|
+
};
|
|
1479
|
+
const labelStyle = {
|
|
1480
|
+
minWidth: 0,
|
|
1481
|
+
overflow: "hidden",
|
|
1482
|
+
textOverflow: "ellipsis",
|
|
1483
|
+
whiteSpace: "nowrap",
|
|
1484
|
+
fontSize: `${props.fontSize}px`,
|
|
1485
|
+
letterSpacing: "0.02em",
|
|
1486
|
+
fontVariantLigatures: "none",
|
|
1487
|
+
...props.color != null ? { color: props.color } : null
|
|
1488
|
+
};
|
|
1489
|
+
return (0, react.createElement)("div", { style: barStyle }, (0, react.createElement)("span", {
|
|
1490
|
+
key: "lang",
|
|
1491
|
+
style: labelStyle
|
|
1492
|
+
}, props.label), (0, react.createElement)(CopyButton, {
|
|
1493
|
+
key: "copy",
|
|
1494
|
+
value: props.value,
|
|
1495
|
+
fontSize: props.fontSize,
|
|
1496
|
+
color: props.color,
|
|
1497
|
+
borderColor: props.borderColor,
|
|
1498
|
+
background: props.buttonBackground
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1378
1501
|
function renderCodeBlock(props) {
|
|
1379
1502
|
const pad = props.padding ?? 0;
|
|
1380
1503
|
const profile = props.highlight === false ? null : profileFor(props.lang);
|
|
1504
|
+
const header = props.header;
|
|
1381
1505
|
const preStyle = {
|
|
1382
1506
|
margin: 0,
|
|
1383
1507
|
padding: `${pad}px`,
|
|
@@ -1386,7 +1510,10 @@ function renderCodeBlock(props) {
|
|
|
1386
1510
|
boxSizing: "border-box",
|
|
1387
1511
|
...props.background != null ? { background: props.background } : null,
|
|
1388
1512
|
...props.color != null ? { color: props.color } : null,
|
|
1389
|
-
...props.radius != null ? { borderRadius: `${props.radius}px` } :
|
|
1513
|
+
...props.radius != null ? header == null ? { borderRadius: `${props.radius}px` } : {
|
|
1514
|
+
borderBottomLeftRadius: `${props.radius}px`,
|
|
1515
|
+
borderBottomRightRadius: `${props.radius}px`
|
|
1516
|
+
} : null,
|
|
1390
1517
|
...profile != null ? {
|
|
1391
1518
|
position: "relative",
|
|
1392
1519
|
tabSize: 8
|
|
@@ -1399,17 +1526,13 @@ function renderCodeBlock(props) {
|
|
|
1399
1526
|
margin: 0,
|
|
1400
1527
|
padding: 0
|
|
1401
1528
|
};
|
|
1402
|
-
|
|
1403
|
-
className: props.className,
|
|
1529
|
+
const pre = (0, react.createElement)("pre", {
|
|
1530
|
+
...header == null ? { className: props.className } : null,
|
|
1404
1531
|
style: preStyle
|
|
1405
|
-
}, (0, react.createElement)("code", {
|
|
1532
|
+
}, profile == null ? (0, react.createElement)("code", {
|
|
1406
1533
|
style: codeStyle,
|
|
1407
1534
|
...props.lang ? { "data-lang": props.lang } : null
|
|
1408
|
-
}, props.value))
|
|
1409
|
-
return (0, react.createElement)("pre", {
|
|
1410
|
-
className: props.className,
|
|
1411
|
-
style: preStyle
|
|
1412
|
-
}, (0, react.createElement)(HighlightedCode, {
|
|
1535
|
+
}, props.value) : (0, react.createElement)(HighlightedCode, {
|
|
1413
1536
|
value: props.value,
|
|
1414
1537
|
lang: props.lang,
|
|
1415
1538
|
font: props.font,
|
|
@@ -1419,6 +1542,20 @@ function renderCodeBlock(props) {
|
|
|
1419
1542
|
colors: resolveTokenColors(props.highlight === false ? void 0 : props.highlight),
|
|
1420
1543
|
codeStyle
|
|
1421
1544
|
}));
|
|
1545
|
+
if (header == null) return pre;
|
|
1546
|
+
return (0, react.createElement)("div", { className: props.className }, (0, react.createElement)(CodeHeader, {
|
|
1547
|
+
key: "header",
|
|
1548
|
+
label: header.label ?? props.lang ?? "code",
|
|
1549
|
+
value: props.value,
|
|
1550
|
+
height: header.height,
|
|
1551
|
+
fontSize: header.fontSize,
|
|
1552
|
+
fontFamily: header.fontFamily ?? "monospace",
|
|
1553
|
+
...props.radius != null ? { radius: props.radius } : null,
|
|
1554
|
+
background: header.background,
|
|
1555
|
+
color: header.color,
|
|
1556
|
+
borderColor: header.borderColor,
|
|
1557
|
+
buttonBackground: header.buttonBackground
|
|
1558
|
+
}), pre);
|
|
1422
1559
|
}
|
|
1423
1560
|
/** A measurable fenced-code primitive (no wrapping; height from line count). */
|
|
1424
1561
|
const CodeBlock = (0, _wingleeio_mugen.markPrimitive)(renderCodeBlock, {
|
|
@@ -1632,7 +1769,16 @@ const defaultComponents = {
|
|
|
1632
1769
|
background: c.background,
|
|
1633
1770
|
radius: c.radius,
|
|
1634
1771
|
highlight: c.highlight,
|
|
1635
|
-
...c.color !== "inherit" ? { color: c.color } : null
|
|
1772
|
+
...c.color !== "inherit" ? { color: c.color } : null,
|
|
1773
|
+
...c.header.show ? { header: {
|
|
1774
|
+
height: c.header.height,
|
|
1775
|
+
fontSize: c.header.fontSize,
|
|
1776
|
+
fontFamily: ctx.theme.monoFamily,
|
|
1777
|
+
background: c.header.background,
|
|
1778
|
+
color: c.header.color,
|
|
1779
|
+
borderColor: c.header.borderColor,
|
|
1780
|
+
buttonBackground: c.header.buttonBackground
|
|
1781
|
+
} } : null
|
|
1636
1782
|
});
|
|
1637
1783
|
},
|
|
1638
1784
|
list: ({ node, ctx }) => renderList(node, ctx),
|
|
@@ -1817,6 +1963,256 @@ function renderMarkdown(source, options = {}) {
|
|
|
1817
1963
|
return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
|
|
1818
1964
|
}
|
|
1819
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
|
|
1820
2216
|
//#region src/markdown.tsx
|
|
1821
2217
|
/**
|
|
1822
2218
|
* Render markdown as a tree of mugen primitives.
|
|
@@ -1839,7 +2235,9 @@ function renderMarkdown(source, options = {}) {
|
|
|
1839
2235
|
* and a deep-partial `theme`.
|
|
1840
2236
|
*/
|
|
1841
2237
|
function Markdown(props) {
|
|
1842
|
-
|
|
2238
|
+
const content = renderMarkdown(props.source, props);
|
|
2239
|
+
if (props.fade && content != null) return (0, react.createElement)(FadeMarkdown, null, content);
|
|
2240
|
+
return content;
|
|
1843
2241
|
}
|
|
1844
2242
|
Markdown.displayName = "Markdown";
|
|
1845
2243
|
//#endregion
|
|
@@ -1860,6 +2258,7 @@ function defineMarkdownComponents(components) {
|
|
|
1860
2258
|
}
|
|
1861
2259
|
//#endregion
|
|
1862
2260
|
exports.CodeBlock = CodeBlock;
|
|
2261
|
+
exports.FadeMarkdown = FadeMarkdown;
|
|
1863
2262
|
Object.defineProperty(exports, "HStack", {
|
|
1864
2263
|
enumerable: true,
|
|
1865
2264
|
get: function() {
|
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,21 @@ interface MarkdownTheme {
|
|
|
110
110
|
* change a block's measured height.
|
|
111
111
|
*/
|
|
112
112
|
highlight: CodeTokenColors | false;
|
|
113
|
+
/**
|
|
114
|
+
* Optional chrome bar above the code — the language on the left, a
|
|
115
|
+
* copy-to-clipboard button on the right. `show: false` (the default) keeps
|
|
116
|
+
* the bare `<pre>`. When shown, the bar's fixed `height` is folded into the
|
|
117
|
+
* block's measured height, so computed and painted heights stay identical.
|
|
118
|
+
*/
|
|
119
|
+
header: {
|
|
120
|
+
/** Render the chrome bar. Off by default. */show: boolean; /** Fixed bar height in px (counted in the measured height). */
|
|
121
|
+
height: number; /** Label + button font size in px. */
|
|
122
|
+
fontSize: number; /** Bar background. */
|
|
123
|
+
background: string; /** Label + button text colour. */
|
|
124
|
+
color: string; /** Bottom hairline + button border colour. */
|
|
125
|
+
borderColor: string; /** Copy-button fill. */
|
|
126
|
+
buttonBackground: string;
|
|
127
|
+
};
|
|
113
128
|
};
|
|
114
129
|
blockquote: {
|
|
115
130
|
padding: number;
|
|
@@ -354,6 +369,13 @@ declare function renderMarkdown(source: string, options?: RenderMarkdownOptions)
|
|
|
354
369
|
interface MarkdownProps extends RenderMarkdownOptions {
|
|
355
370
|
/** The markdown source to render. */
|
|
356
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;
|
|
357
379
|
}
|
|
358
380
|
/**
|
|
359
381
|
* Render markdown as a tree of mugen primitives.
|
|
@@ -383,8 +405,9 @@ declare namespace Markdown {
|
|
|
383
405
|
//#region src/primitives/code-block.d.ts
|
|
384
406
|
/**
|
|
385
407
|
* A fenced code block. Code does not wrap — long lines scroll horizontally — so
|
|
386
|
-
* its height is simply `lineCount × lineHeight + 2 × padding
|
|
387
|
-
*
|
|
408
|
+
* its height is simply `lineCount × lineHeight + 2 × padding` (plus an optional
|
|
409
|
+
* fixed-height {@link CodeBlockHeader} bar), independent of the row width. That
|
|
410
|
+
* makes it trivially and exactly measurable.
|
|
388
411
|
*
|
|
389
412
|
* Syntax highlighting is layered on as pure paint, never layout: the `<code>`
|
|
390
413
|
* text renders immediately (plain, selectable, accessible), the language is
|
|
@@ -393,6 +416,27 @@ declare namespace Markdown {
|
|
|
393
416
|
* turns `color: transparent` in the same frame. Highlighting therefore can't
|
|
394
417
|
* block first paint and can't ever change the measured height.
|
|
395
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* Optional chrome bar above the code: the language on the left, a
|
|
421
|
+
* copy-to-clipboard button on the right. Its fixed `height` is added to the
|
|
422
|
+
* block's measured height, so the block still measures exactly what it paints.
|
|
423
|
+
* Pure decoration otherwise — the bar never wraps and never grows.
|
|
424
|
+
*/
|
|
425
|
+
interface CodeBlockHeader {
|
|
426
|
+
/** Left-aligned label; falls back to {@link CodeBlockProps.lang}, then `code`. */
|
|
427
|
+
label?: string;
|
|
428
|
+
/** Fixed bar height in px (folded into the measured height). */
|
|
429
|
+
height: number;
|
|
430
|
+
/** Label + button font size in px. */
|
|
431
|
+
fontSize: number;
|
|
432
|
+
/** Monospace family for the label/button; defaults to `monospace`. */
|
|
433
|
+
fontFamily?: string;
|
|
434
|
+
background?: string;
|
|
435
|
+
color?: string;
|
|
436
|
+
borderColor?: string;
|
|
437
|
+
/** Copy-button fill. */
|
|
438
|
+
buttonBackground?: string;
|
|
439
|
+
}
|
|
396
440
|
interface CodeBlockProps<C extends string = string> {
|
|
397
441
|
/** Raw code text. Newlines determine the line count. */
|
|
398
442
|
value: string;
|
|
@@ -413,6 +457,11 @@ interface CodeBlockProps<C extends string = string> {
|
|
|
413
457
|
* registered profile are highlighted either way.
|
|
414
458
|
*/
|
|
415
459
|
highlight?: Partial<CodeTokenColors> | false;
|
|
460
|
+
/**
|
|
461
|
+
* A chrome bar above the code (language label + copy button). Omit for the
|
|
462
|
+
* bare `<pre>`. Its `height` is folded into the measured height.
|
|
463
|
+
*/
|
|
464
|
+
header?: CodeBlockHeader;
|
|
416
465
|
style?: MeasurableStyle;
|
|
417
466
|
className?: SafeClassName<C>;
|
|
418
467
|
}
|
|
@@ -454,6 +503,18 @@ interface TableBlockProps<C extends string = string> {
|
|
|
454
503
|
/** A measurable GFM-table primitive with aligned, content-proportional columns. */
|
|
455
504
|
declare const TableBlock: <C extends string = string>(props: TableBlockProps<C>) => ReactElement;
|
|
456
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
|
|
457
518
|
//#region src/highlight/languages.d.ts
|
|
458
519
|
/**
|
|
459
520
|
* Language profiles for the built-in tokenizer. A profile is a small data
|
|
@@ -495,4 +556,4 @@ declare function registerLanguage(names: string | readonly string[], p: Language
|
|
|
495
556
|
/** The profile for a fence language, or `null` when the language is unknown. */
|
|
496
557
|
declare function profileFor(lang: string | undefined): LanguageProfile | null;
|
|
497
558
|
//#endregion
|
|
498
|
-
export { type Blockquote, type BoxProps, type Code, CodeBlock, 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
|
@@ -110,6 +110,21 @@ interface MarkdownTheme {
|
|
|
110
110
|
* change a block's measured height.
|
|
111
111
|
*/
|
|
112
112
|
highlight: CodeTokenColors | false;
|
|
113
|
+
/**
|
|
114
|
+
* Optional chrome bar above the code — the language on the left, a
|
|
115
|
+
* copy-to-clipboard button on the right. `show: false` (the default) keeps
|
|
116
|
+
* the bare `<pre>`. When shown, the bar's fixed `height` is folded into the
|
|
117
|
+
* block's measured height, so computed and painted heights stay identical.
|
|
118
|
+
*/
|
|
119
|
+
header: {
|
|
120
|
+
/** Render the chrome bar. Off by default. */show: boolean; /** Fixed bar height in px (counted in the measured height). */
|
|
121
|
+
height: number; /** Label + button font size in px. */
|
|
122
|
+
fontSize: number; /** Bar background. */
|
|
123
|
+
background: string; /** Label + button text colour. */
|
|
124
|
+
color: string; /** Bottom hairline + button border colour. */
|
|
125
|
+
borderColor: string; /** Copy-button fill. */
|
|
126
|
+
buttonBackground: string;
|
|
127
|
+
};
|
|
113
128
|
};
|
|
114
129
|
blockquote: {
|
|
115
130
|
padding: number;
|
|
@@ -354,6 +369,13 @@ declare function renderMarkdown(source: string, options?: RenderMarkdownOptions)
|
|
|
354
369
|
interface MarkdownProps extends RenderMarkdownOptions {
|
|
355
370
|
/** The markdown source to render. */
|
|
356
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;
|
|
357
379
|
}
|
|
358
380
|
/**
|
|
359
381
|
* Render markdown as a tree of mugen primitives.
|
|
@@ -383,8 +405,9 @@ declare namespace Markdown {
|
|
|
383
405
|
//#region src/primitives/code-block.d.ts
|
|
384
406
|
/**
|
|
385
407
|
* A fenced code block. Code does not wrap — long lines scroll horizontally — so
|
|
386
|
-
* its height is simply `lineCount × lineHeight + 2 × padding
|
|
387
|
-
*
|
|
408
|
+
* its height is simply `lineCount × lineHeight + 2 × padding` (plus an optional
|
|
409
|
+
* fixed-height {@link CodeBlockHeader} bar), independent of the row width. That
|
|
410
|
+
* makes it trivially and exactly measurable.
|
|
388
411
|
*
|
|
389
412
|
* Syntax highlighting is layered on as pure paint, never layout: the `<code>`
|
|
390
413
|
* text renders immediately (plain, selectable, accessible), the language is
|
|
@@ -393,6 +416,27 @@ declare namespace Markdown {
|
|
|
393
416
|
* turns `color: transparent` in the same frame. Highlighting therefore can't
|
|
394
417
|
* block first paint and can't ever change the measured height.
|
|
395
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* Optional chrome bar above the code: the language on the left, a
|
|
421
|
+
* copy-to-clipboard button on the right. Its fixed `height` is added to the
|
|
422
|
+
* block's measured height, so the block still measures exactly what it paints.
|
|
423
|
+
* Pure decoration otherwise — the bar never wraps and never grows.
|
|
424
|
+
*/
|
|
425
|
+
interface CodeBlockHeader {
|
|
426
|
+
/** Left-aligned label; falls back to {@link CodeBlockProps.lang}, then `code`. */
|
|
427
|
+
label?: string;
|
|
428
|
+
/** Fixed bar height in px (folded into the measured height). */
|
|
429
|
+
height: number;
|
|
430
|
+
/** Label + button font size in px. */
|
|
431
|
+
fontSize: number;
|
|
432
|
+
/** Monospace family for the label/button; defaults to `monospace`. */
|
|
433
|
+
fontFamily?: string;
|
|
434
|
+
background?: string;
|
|
435
|
+
color?: string;
|
|
436
|
+
borderColor?: string;
|
|
437
|
+
/** Copy-button fill. */
|
|
438
|
+
buttonBackground?: string;
|
|
439
|
+
}
|
|
396
440
|
interface CodeBlockProps<C extends string = string> {
|
|
397
441
|
/** Raw code text. Newlines determine the line count. */
|
|
398
442
|
value: string;
|
|
@@ -413,6 +457,11 @@ interface CodeBlockProps<C extends string = string> {
|
|
|
413
457
|
* registered profile are highlighted either way.
|
|
414
458
|
*/
|
|
415
459
|
highlight?: Partial<CodeTokenColors> | false;
|
|
460
|
+
/**
|
|
461
|
+
* A chrome bar above the code (language label + copy button). Omit for the
|
|
462
|
+
* bare `<pre>`. Its `height` is folded into the measured height.
|
|
463
|
+
*/
|
|
464
|
+
header?: CodeBlockHeader;
|
|
416
465
|
style?: MeasurableStyle;
|
|
417
466
|
className?: SafeClassName<C>;
|
|
418
467
|
}
|
|
@@ -454,6 +503,18 @@ interface TableBlockProps<C extends string = string> {
|
|
|
454
503
|
/** A measurable GFM-table primitive with aligned, content-proportional columns. */
|
|
455
504
|
declare const TableBlock: <C extends string = string>(props: TableBlockProps<C>) => ReactElement;
|
|
456
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
|
|
457
518
|
//#region src/highlight/languages.d.ts
|
|
458
519
|
/**
|
|
459
520
|
* Language profiles for the built-in tokenizer. A profile is a small data
|
|
@@ -495,4 +556,4 @@ declare function registerLanguage(names: string | readonly string[], p: Language
|
|
|
495
556
|
/** The profile for a fence language, or `null` when the language is unknown. */
|
|
496
557
|
declare function profileFor(lang: string | undefined): LanguageProfile | null;
|
|
497
558
|
//#endregion
|
|
498
|
-
export { type Blockquote, type BoxProps, type Code, CodeBlock, 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
|
-
import { Fragment, createElement, useEffect, useLayoutEffect, useRef } from "react";
|
|
2
|
-
import { HStack, HStack as HStack$1, Text, VStack, VStack as VStack$1, assertMeasurableFont, definePrimitive, definePrimitive as definePrimitive$1, fontEpoch, fontLonghands, fontWithLineHeight, markPrimitive, naturalWidthOf } from "@wingleeio/mugen";
|
|
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, 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
|
|
@@ -161,7 +161,16 @@ const defaultTheme = {
|
|
|
161
161
|
background: "rgba(127, 127, 127, 0.12)",
|
|
162
162
|
color: "inherit",
|
|
163
163
|
radius: 8,
|
|
164
|
-
highlight: defaultTokenColors
|
|
164
|
+
highlight: defaultTokenColors,
|
|
165
|
+
header: {
|
|
166
|
+
show: false,
|
|
167
|
+
height: 38,
|
|
168
|
+
fontSize: 12,
|
|
169
|
+
background: "rgba(127, 127, 127, 0.06)",
|
|
170
|
+
color: "rgba(127, 127, 127, 0.85)",
|
|
171
|
+
borderColor: "rgba(127, 127, 127, 0.18)",
|
|
172
|
+
buttonBackground: "rgba(127, 127, 127, 0.04)"
|
|
173
|
+
}
|
|
165
174
|
},
|
|
166
175
|
blockquote: {
|
|
167
176
|
padding: 14,
|
|
@@ -1310,7 +1319,7 @@ function lineCount(value) {
|
|
|
1310
1319
|
function measureCodeBlock(props, ctx) {
|
|
1311
1320
|
assertMeasurableFont(props.font);
|
|
1312
1321
|
const pad = props.padding ?? 0;
|
|
1313
|
-
return lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1322
|
+
return (props.header ? props.header.height : 0) + lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1314
1323
|
}
|
|
1315
1324
|
const colorsCache = /* @__PURE__ */ new WeakMap();
|
|
1316
1325
|
function resolveTokenColors(overrides) {
|
|
@@ -1374,9 +1383,124 @@ function HighlightedCode(props) {
|
|
|
1374
1383
|
style: overlayStyle
|
|
1375
1384
|
}));
|
|
1376
1385
|
}
|
|
1386
|
+
function legacyCopy(text) {
|
|
1387
|
+
if (typeof document === "undefined") return false;
|
|
1388
|
+
const ta = document.createElement("textarea");
|
|
1389
|
+
ta.value = text;
|
|
1390
|
+
ta.setAttribute("readonly", "");
|
|
1391
|
+
ta.style.position = "fixed";
|
|
1392
|
+
ta.style.top = "0";
|
|
1393
|
+
ta.style.left = "0";
|
|
1394
|
+
ta.style.opacity = "0";
|
|
1395
|
+
ta.style.pointerEvents = "none";
|
|
1396
|
+
document.body.appendChild(ta);
|
|
1397
|
+
ta.select();
|
|
1398
|
+
let ok = false;
|
|
1399
|
+
try {
|
|
1400
|
+
ok = document.execCommand("copy");
|
|
1401
|
+
} catch {
|
|
1402
|
+
ok = false;
|
|
1403
|
+
}
|
|
1404
|
+
document.body.removeChild(ta);
|
|
1405
|
+
return ok;
|
|
1406
|
+
}
|
|
1407
|
+
async function copyText(text) {
|
|
1408
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
1409
|
+
if (clip?.writeText != null) try {
|
|
1410
|
+
await clip.writeText(text);
|
|
1411
|
+
return true;
|
|
1412
|
+
} catch {}
|
|
1413
|
+
return legacyCopy(text);
|
|
1414
|
+
}
|
|
1415
|
+
/** Copies the block's raw text; flips to "Copied" for ~1.6s on success. */
|
|
1416
|
+
function CopyButton(props) {
|
|
1417
|
+
const [copied, setCopied] = useState(false);
|
|
1418
|
+
const [hovered, setHovered] = useState(false);
|
|
1419
|
+
const timer = useRef(null);
|
|
1420
|
+
useEffect(() => () => {
|
|
1421
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1422
|
+
}, []);
|
|
1423
|
+
const onClick = () => {
|
|
1424
|
+
copyText(props.value).then((ok) => {
|
|
1425
|
+
if (!ok) return;
|
|
1426
|
+
setCopied(true);
|
|
1427
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1428
|
+
timer.current = setTimeout(() => setCopied(false), 1600);
|
|
1429
|
+
}).catch(() => {});
|
|
1430
|
+
};
|
|
1431
|
+
const style = {
|
|
1432
|
+
display: "inline-flex",
|
|
1433
|
+
flex: "0 0 auto",
|
|
1434
|
+
alignItems: "center",
|
|
1435
|
+
justifyContent: "center",
|
|
1436
|
+
whiteSpace: "nowrap",
|
|
1437
|
+
cursor: "pointer",
|
|
1438
|
+
borderRadius: 8,
|
|
1439
|
+
border: `1px solid ${props.borderColor ?? "rgba(127, 127, 127, 0.2)"}`,
|
|
1440
|
+
...props.background != null ? { background: props.background } : null,
|
|
1441
|
+
...props.color != null ? { color: props.color } : null,
|
|
1442
|
+
padding: "4px 9px",
|
|
1443
|
+
minWidth: "4.6em",
|
|
1444
|
+
fontFamily: "inherit",
|
|
1445
|
+
fontSize: `${props.fontSize}px`,
|
|
1446
|
+
lineHeight: 1,
|
|
1447
|
+
opacity: hovered || copied ? 1 : .82,
|
|
1448
|
+
transition: "opacity 120ms ease"
|
|
1449
|
+
};
|
|
1450
|
+
return createElement("button", {
|
|
1451
|
+
type: "button",
|
|
1452
|
+
onClick,
|
|
1453
|
+
onMouseEnter: () => setHovered(true),
|
|
1454
|
+
onMouseLeave: () => setHovered(false),
|
|
1455
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
1456
|
+
style
|
|
1457
|
+
}, copied ? "Copied" : "Copy");
|
|
1458
|
+
}
|
|
1459
|
+
/** The fixed-height chrome bar: language label left, copy button right. */
|
|
1460
|
+
function CodeHeader(props) {
|
|
1461
|
+
const barStyle = {
|
|
1462
|
+
display: "flex",
|
|
1463
|
+
flex: "0 0 auto",
|
|
1464
|
+
alignItems: "center",
|
|
1465
|
+
justifyContent: "space-between",
|
|
1466
|
+
gap: 8,
|
|
1467
|
+
height: `${props.height}px`,
|
|
1468
|
+
boxSizing: "border-box",
|
|
1469
|
+
padding: "0 12px",
|
|
1470
|
+
fontFamily: props.fontFamily,
|
|
1471
|
+
...props.radius != null ? {
|
|
1472
|
+
borderTopLeftRadius: `${props.radius}px`,
|
|
1473
|
+
borderTopRightRadius: `${props.radius}px`
|
|
1474
|
+
} : null,
|
|
1475
|
+
...props.background != null ? { background: props.background } : null,
|
|
1476
|
+
...props.borderColor != null ? { borderBottom: `1px solid ${props.borderColor}` } : null
|
|
1477
|
+
};
|
|
1478
|
+
const labelStyle = {
|
|
1479
|
+
minWidth: 0,
|
|
1480
|
+
overflow: "hidden",
|
|
1481
|
+
textOverflow: "ellipsis",
|
|
1482
|
+
whiteSpace: "nowrap",
|
|
1483
|
+
fontSize: `${props.fontSize}px`,
|
|
1484
|
+
letterSpacing: "0.02em",
|
|
1485
|
+
fontVariantLigatures: "none",
|
|
1486
|
+
...props.color != null ? { color: props.color } : null
|
|
1487
|
+
};
|
|
1488
|
+
return createElement("div", { style: barStyle }, createElement("span", {
|
|
1489
|
+
key: "lang",
|
|
1490
|
+
style: labelStyle
|
|
1491
|
+
}, props.label), createElement(CopyButton, {
|
|
1492
|
+
key: "copy",
|
|
1493
|
+
value: props.value,
|
|
1494
|
+
fontSize: props.fontSize,
|
|
1495
|
+
color: props.color,
|
|
1496
|
+
borderColor: props.borderColor,
|
|
1497
|
+
background: props.buttonBackground
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1377
1500
|
function renderCodeBlock(props) {
|
|
1378
1501
|
const pad = props.padding ?? 0;
|
|
1379
1502
|
const profile = props.highlight === false ? null : profileFor(props.lang);
|
|
1503
|
+
const header = props.header;
|
|
1380
1504
|
const preStyle = {
|
|
1381
1505
|
margin: 0,
|
|
1382
1506
|
padding: `${pad}px`,
|
|
@@ -1385,7 +1509,10 @@ function renderCodeBlock(props) {
|
|
|
1385
1509
|
boxSizing: "border-box",
|
|
1386
1510
|
...props.background != null ? { background: props.background } : null,
|
|
1387
1511
|
...props.color != null ? { color: props.color } : null,
|
|
1388
|
-
...props.radius != null ? { borderRadius: `${props.radius}px` } :
|
|
1512
|
+
...props.radius != null ? header == null ? { borderRadius: `${props.radius}px` } : {
|
|
1513
|
+
borderBottomLeftRadius: `${props.radius}px`,
|
|
1514
|
+
borderBottomRightRadius: `${props.radius}px`
|
|
1515
|
+
} : null,
|
|
1389
1516
|
...profile != null ? {
|
|
1390
1517
|
position: "relative",
|
|
1391
1518
|
tabSize: 8
|
|
@@ -1398,17 +1525,13 @@ function renderCodeBlock(props) {
|
|
|
1398
1525
|
margin: 0,
|
|
1399
1526
|
padding: 0
|
|
1400
1527
|
};
|
|
1401
|
-
|
|
1402
|
-
className: props.className,
|
|
1528
|
+
const pre = createElement("pre", {
|
|
1529
|
+
...header == null ? { className: props.className } : null,
|
|
1403
1530
|
style: preStyle
|
|
1404
|
-
}, createElement("code", {
|
|
1531
|
+
}, profile == null ? createElement("code", {
|
|
1405
1532
|
style: codeStyle,
|
|
1406
1533
|
...props.lang ? { "data-lang": props.lang } : null
|
|
1407
|
-
}, props.value)
|
|
1408
|
-
return createElement("pre", {
|
|
1409
|
-
className: props.className,
|
|
1410
|
-
style: preStyle
|
|
1411
|
-
}, createElement(HighlightedCode, {
|
|
1534
|
+
}, props.value) : createElement(HighlightedCode, {
|
|
1412
1535
|
value: props.value,
|
|
1413
1536
|
lang: props.lang,
|
|
1414
1537
|
font: props.font,
|
|
@@ -1418,6 +1541,20 @@ function renderCodeBlock(props) {
|
|
|
1418
1541
|
colors: resolveTokenColors(props.highlight === false ? void 0 : props.highlight),
|
|
1419
1542
|
codeStyle
|
|
1420
1543
|
}));
|
|
1544
|
+
if (header == null) return pre;
|
|
1545
|
+
return createElement("div", { className: props.className }, createElement(CodeHeader, {
|
|
1546
|
+
key: "header",
|
|
1547
|
+
label: header.label ?? props.lang ?? "code",
|
|
1548
|
+
value: props.value,
|
|
1549
|
+
height: header.height,
|
|
1550
|
+
fontSize: header.fontSize,
|
|
1551
|
+
fontFamily: header.fontFamily ?? "monospace",
|
|
1552
|
+
...props.radius != null ? { radius: props.radius } : null,
|
|
1553
|
+
background: header.background,
|
|
1554
|
+
color: header.color,
|
|
1555
|
+
borderColor: header.borderColor,
|
|
1556
|
+
buttonBackground: header.buttonBackground
|
|
1557
|
+
}), pre);
|
|
1421
1558
|
}
|
|
1422
1559
|
/** A measurable fenced-code primitive (no wrapping; height from line count). */
|
|
1423
1560
|
const CodeBlock = markPrimitive(renderCodeBlock, {
|
|
@@ -1631,7 +1768,16 @@ const defaultComponents = {
|
|
|
1631
1768
|
background: c.background,
|
|
1632
1769
|
radius: c.radius,
|
|
1633
1770
|
highlight: c.highlight,
|
|
1634
|
-
...c.color !== "inherit" ? { color: c.color } : null
|
|
1771
|
+
...c.color !== "inherit" ? { color: c.color } : null,
|
|
1772
|
+
...c.header.show ? { header: {
|
|
1773
|
+
height: c.header.height,
|
|
1774
|
+
fontSize: c.header.fontSize,
|
|
1775
|
+
fontFamily: ctx.theme.monoFamily,
|
|
1776
|
+
background: c.header.background,
|
|
1777
|
+
color: c.header.color,
|
|
1778
|
+
borderColor: c.header.borderColor,
|
|
1779
|
+
buttonBackground: c.header.buttonBackground
|
|
1780
|
+
} } : null
|
|
1635
1781
|
});
|
|
1636
1782
|
},
|
|
1637
1783
|
list: ({ node, ctx }) => renderList(node, ctx),
|
|
@@ -1816,6 +1962,256 @@ function renderMarkdown(source, options = {}) {
|
|
|
1816
1962
|
return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
|
|
1817
1963
|
}
|
|
1818
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
|
|
1819
2215
|
//#region src/markdown.tsx
|
|
1820
2216
|
/**
|
|
1821
2217
|
* Render markdown as a tree of mugen primitives.
|
|
@@ -1838,7 +2234,9 @@ function renderMarkdown(source, options = {}) {
|
|
|
1838
2234
|
* and a deep-partial `theme`.
|
|
1839
2235
|
*/
|
|
1840
2236
|
function Markdown(props) {
|
|
1841
|
-
|
|
2237
|
+
const content = renderMarkdown(props.source, props);
|
|
2238
|
+
if (props.fade && content != null) return createElement(FadeMarkdown, null, content);
|
|
2239
|
+
return content;
|
|
1842
2240
|
}
|
|
1843
2241
|
Markdown.displayName = "Markdown";
|
|
1844
2242
|
//#endregion
|
|
@@ -1858,4 +2256,4 @@ function defineMarkdownComponents(components) {
|
|
|
1858
2256
|
return components;
|
|
1859
2257
|
}
|
|
1860
2258
|
//#endregion
|
|
1861
|
-
export { CodeBlock, HStack, Markdown, RichText, TableBlock, Text, VStack, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
|
|
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>",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"tsdown": "^0.22.2",
|
|
66
66
|
"typescript": "^6.0.3",
|
|
67
67
|
"vitest": "^4.1.8",
|
|
68
|
-
"@wingleeio/mugen": "0.3.
|
|
68
|
+
"@wingleeio/mugen": "0.3.4"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsdown",
|