@wingleeio/mugen-markdown 0.4.1 → 0.4.3
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 +68 -15
- package/dist/index.mjs +68 -15
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -2041,10 +2041,16 @@ function renderMarkdown(source, options = {}) {
|
|
|
2041
2041
|
* text fading in. Nothing about the row ever animates; layout is done the
|
|
2042
2042
|
* moment the text lands.
|
|
2043
2043
|
*
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2044
|
+
* The veil canvas lives **inside** the markdown's own box (`position:
|
|
2045
|
+
* absolute`), so it scrolls with the content. It is *windowed to the scroll
|
|
2046
|
+
* viewport* each frame — sized and positioned to cover only the visible band,
|
|
2047
|
+
* never the full answer height — because the backing store is reallocated and
|
|
2048
|
+
* `clearRect`-cleared every frame, and a full-height canvas on a tall stream
|
|
2049
|
+
* makes that O(answer length): a multi-megapixel clear at 60fps that blows the
|
|
2050
|
+
* frame budget once the answer is tall. The veils only ever sit on the freshly
|
|
2051
|
+
* appended tail, which stick-to-bottom keeps at the viewport's edge, so the
|
|
2052
|
+
* window loses nothing. The painter also idles (no rAF) until a DOM mutation
|
|
2053
|
+
* arrives, so leaving `fade` on for a settled block costs nothing.
|
|
2048
2054
|
*/
|
|
2049
2055
|
const EMA_SEED_MS = 160;
|
|
2050
2056
|
const MIN_FADE_MS = 120;
|
|
@@ -2059,6 +2065,10 @@ function elementInChrome(el, container) {
|
|
|
2059
2065
|
for (let p = el; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
|
|
2060
2066
|
return false;
|
|
2061
2067
|
}
|
|
2068
|
+
/** Whether a computed `overflow` value clips its overflow (bounds visibility). */
|
|
2069
|
+
function isClipped(overflow) {
|
|
2070
|
+
return overflow === "auto" || overflow === "scroll" || overflow === "hidden" || overflow === "clip" || overflow === "overlay";
|
|
2071
|
+
}
|
|
2062
2072
|
function contentTextFilter(container) {
|
|
2063
2073
|
return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
|
|
2064
2074
|
}
|
|
@@ -2147,6 +2157,9 @@ var FadePainter = class {
|
|
|
2147
2157
|
raf = 0;
|
|
2148
2158
|
running = false;
|
|
2149
2159
|
mo = null;
|
|
2160
|
+
/** Scrollable/clipping ancestors of the content — found once, then cached.
|
|
2161
|
+
* Intersecting their rects bounds the veil canvas to the visible band. */
|
|
2162
|
+
clippers = null;
|
|
2150
2163
|
attach(content, canvas) {
|
|
2151
2164
|
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
2152
2165
|
const ctx = canvas.getContext("2d");
|
|
@@ -2192,6 +2205,36 @@ var FadePainter = class {
|
|
|
2192
2205
|
this.running = true;
|
|
2193
2206
|
this.raf = requestAnimationFrame(this.frame);
|
|
2194
2207
|
}
|
|
2208
|
+
/**
|
|
2209
|
+
* The vertical band of `content` currently inside the scroll viewport, in
|
|
2210
|
+
* client coordinates, clamped to the content's own box. Intersects every
|
|
2211
|
+
* scrollable/clipping ancestor (found once via `getComputedStyle`, then
|
|
2212
|
+
* cached) and the window. This is what keeps the veil canvas O(viewport)
|
|
2213
|
+
* instead of O(content height): only the visible band is sized and cleared.
|
|
2214
|
+
*/
|
|
2215
|
+
visibleBand(content, rect) {
|
|
2216
|
+
if (this.clippers == null) {
|
|
2217
|
+
const list = [];
|
|
2218
|
+
for (let p = content.parentElement; p != null; p = p.parentElement) {
|
|
2219
|
+
const s = getComputedStyle(p);
|
|
2220
|
+
if (isClipped(s.overflowY) || isClipped(s.overflowX)) list.push(p);
|
|
2221
|
+
}
|
|
2222
|
+
this.clippers = list;
|
|
2223
|
+
}
|
|
2224
|
+
let top = 0;
|
|
2225
|
+
let bottom = typeof innerHeight === "number" ? innerHeight : rect.bottom;
|
|
2226
|
+
for (const c of this.clippers) {
|
|
2227
|
+
const r = c.getBoundingClientRect();
|
|
2228
|
+
if (r.top > top) top = r.top;
|
|
2229
|
+
if (r.bottom < bottom) bottom = r.bottom;
|
|
2230
|
+
}
|
|
2231
|
+
const visTop = Math.max(rect.top, top);
|
|
2232
|
+
const visBottom = Math.min(rect.bottom, bottom);
|
|
2233
|
+
return {
|
|
2234
|
+
top: visTop,
|
|
2235
|
+
height: Math.max(0, visBottom - visTop)
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2195
2238
|
frame = () => {
|
|
2196
2239
|
const content = this.content;
|
|
2197
2240
|
const canvas = this.canvas;
|
|
@@ -2224,8 +2267,12 @@ var FadePainter = class {
|
|
|
2224
2267
|
const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
|
|
2225
2268
|
this.veils = this.veils.filter((v) => (now - v.t0) * boost < duration);
|
|
2226
2269
|
const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
2227
|
-
const
|
|
2228
|
-
const
|
|
2270
|
+
const rect = content.getBoundingClientRect();
|
|
2271
|
+
const band = this.visibleBand(content, rect);
|
|
2272
|
+
const w = rect.width;
|
|
2273
|
+
const h = band.height;
|
|
2274
|
+
canvas.style.top = `${band.top - rect.top}px`;
|
|
2275
|
+
canvas.style.height = `${h}px`;
|
|
2229
2276
|
if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
|
|
2230
2277
|
canvas.width = Math.round(w * dpr);
|
|
2231
2278
|
canvas.height = Math.round(h * dpr);
|
|
@@ -2233,16 +2280,21 @@ var FadePainter = class {
|
|
|
2233
2280
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2234
2281
|
ctx.clearRect(0, 0, w, h);
|
|
2235
2282
|
if (this.veils.length > 0) {
|
|
2236
|
-
const origin =
|
|
2283
|
+
const origin = {
|
|
2284
|
+
left: rect.left,
|
|
2285
|
+
top: band.top
|
|
2286
|
+
};
|
|
2237
2287
|
const bgCache = /* @__PURE__ */ new Map();
|
|
2238
2288
|
const groups = /* @__PURE__ */ new Map();
|
|
2239
2289
|
const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
|
|
2240
2290
|
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
|
|
2241
|
-
|
|
2242
|
-
let node = walker.
|
|
2243
|
-
|
|
2291
|
+
walker.currentNode = content;
|
|
2292
|
+
let node = walker.lastChild();
|
|
2293
|
+
let end = this.length;
|
|
2294
|
+
while (node != null && end > minStart) {
|
|
2244
2295
|
const len = node.data.length;
|
|
2245
|
-
|
|
2296
|
+
const base = end - len;
|
|
2297
|
+
if (len > 0) for (let vi = 0; vi < this.veils.length; vi++) {
|
|
2246
2298
|
const v = this.veils[vi];
|
|
2247
2299
|
const s = Math.max(v.start - base, 0);
|
|
2248
2300
|
const e = Math.min(v.end - base, len);
|
|
@@ -2266,8 +2318,8 @@ var FadePainter = class {
|
|
|
2266
2318
|
range.setEnd(node, e);
|
|
2267
2319
|
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);
|
|
2268
2320
|
}
|
|
2269
|
-
|
|
2270
|
-
node = walker.
|
|
2321
|
+
end = base;
|
|
2322
|
+
node = walker.previousNode();
|
|
2271
2323
|
}
|
|
2272
2324
|
for (const group of groups.values()) {
|
|
2273
2325
|
ctx.globalAlpha = group.alpha;
|
|
@@ -2300,9 +2352,10 @@ function renderFadeMarkdown(props) {
|
|
|
2300
2352
|
"aria-hidden": true,
|
|
2301
2353
|
style: {
|
|
2302
2354
|
position: "absolute",
|
|
2303
|
-
|
|
2355
|
+
left: 0,
|
|
2356
|
+
top: 0,
|
|
2304
2357
|
width: "100%",
|
|
2305
|
-
height:
|
|
2358
|
+
height: 0,
|
|
2306
2359
|
pointerEvents: "none"
|
|
2307
2360
|
}
|
|
2308
2361
|
}));
|
package/dist/index.mjs
CHANGED
|
@@ -2040,10 +2040,16 @@ function renderMarkdown(source, options = {}) {
|
|
|
2040
2040
|
* text fading in. Nothing about the row ever animates; layout is done the
|
|
2041
2041
|
* moment the text lands.
|
|
2042
2042
|
*
|
|
2043
|
-
*
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2043
|
+
* The veil canvas lives **inside** the markdown's own box (`position:
|
|
2044
|
+
* absolute`), so it scrolls with the content. It is *windowed to the scroll
|
|
2045
|
+
* viewport* each frame — sized and positioned to cover only the visible band,
|
|
2046
|
+
* never the full answer height — because the backing store is reallocated and
|
|
2047
|
+
* `clearRect`-cleared every frame, and a full-height canvas on a tall stream
|
|
2048
|
+
* makes that O(answer length): a multi-megapixel clear at 60fps that blows the
|
|
2049
|
+
* frame budget once the answer is tall. The veils only ever sit on the freshly
|
|
2050
|
+
* appended tail, which stick-to-bottom keeps at the viewport's edge, so the
|
|
2051
|
+
* window loses nothing. The painter also idles (no rAF) until a DOM mutation
|
|
2052
|
+
* arrives, so leaving `fade` on for a settled block costs nothing.
|
|
2047
2053
|
*/
|
|
2048
2054
|
const EMA_SEED_MS = 160;
|
|
2049
2055
|
const MIN_FADE_MS = 120;
|
|
@@ -2058,6 +2064,10 @@ function elementInChrome(el, container) {
|
|
|
2058
2064
|
for (let p = el; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
|
|
2059
2065
|
return false;
|
|
2060
2066
|
}
|
|
2067
|
+
/** Whether a computed `overflow` value clips its overflow (bounds visibility). */
|
|
2068
|
+
function isClipped(overflow) {
|
|
2069
|
+
return overflow === "auto" || overflow === "scroll" || overflow === "hidden" || overflow === "clip" || overflow === "overlay";
|
|
2070
|
+
}
|
|
2061
2071
|
function contentTextFilter(container) {
|
|
2062
2072
|
return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
|
|
2063
2073
|
}
|
|
@@ -2146,6 +2156,9 @@ var FadePainter = class {
|
|
|
2146
2156
|
raf = 0;
|
|
2147
2157
|
running = false;
|
|
2148
2158
|
mo = null;
|
|
2159
|
+
/** Scrollable/clipping ancestors of the content — found once, then cached.
|
|
2160
|
+
* Intersecting their rects bounds the veil canvas to the visible band. */
|
|
2161
|
+
clippers = null;
|
|
2149
2162
|
attach(content, canvas) {
|
|
2150
2163
|
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
2151
2164
|
const ctx = canvas.getContext("2d");
|
|
@@ -2191,6 +2204,36 @@ var FadePainter = class {
|
|
|
2191
2204
|
this.running = true;
|
|
2192
2205
|
this.raf = requestAnimationFrame(this.frame);
|
|
2193
2206
|
}
|
|
2207
|
+
/**
|
|
2208
|
+
* The vertical band of `content` currently inside the scroll viewport, in
|
|
2209
|
+
* client coordinates, clamped to the content's own box. Intersects every
|
|
2210
|
+
* scrollable/clipping ancestor (found once via `getComputedStyle`, then
|
|
2211
|
+
* cached) and the window. This is what keeps the veil canvas O(viewport)
|
|
2212
|
+
* instead of O(content height): only the visible band is sized and cleared.
|
|
2213
|
+
*/
|
|
2214
|
+
visibleBand(content, rect) {
|
|
2215
|
+
if (this.clippers == null) {
|
|
2216
|
+
const list = [];
|
|
2217
|
+
for (let p = content.parentElement; p != null; p = p.parentElement) {
|
|
2218
|
+
const s = getComputedStyle(p);
|
|
2219
|
+
if (isClipped(s.overflowY) || isClipped(s.overflowX)) list.push(p);
|
|
2220
|
+
}
|
|
2221
|
+
this.clippers = list;
|
|
2222
|
+
}
|
|
2223
|
+
let top = 0;
|
|
2224
|
+
let bottom = typeof innerHeight === "number" ? innerHeight : rect.bottom;
|
|
2225
|
+
for (const c of this.clippers) {
|
|
2226
|
+
const r = c.getBoundingClientRect();
|
|
2227
|
+
if (r.top > top) top = r.top;
|
|
2228
|
+
if (r.bottom < bottom) bottom = r.bottom;
|
|
2229
|
+
}
|
|
2230
|
+
const visTop = Math.max(rect.top, top);
|
|
2231
|
+
const visBottom = Math.min(rect.bottom, bottom);
|
|
2232
|
+
return {
|
|
2233
|
+
top: visTop,
|
|
2234
|
+
height: Math.max(0, visBottom - visTop)
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2194
2237
|
frame = () => {
|
|
2195
2238
|
const content = this.content;
|
|
2196
2239
|
const canvas = this.canvas;
|
|
@@ -2223,8 +2266,12 @@ var FadePainter = class {
|
|
|
2223
2266
|
const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
|
|
2224
2267
|
this.veils = this.veils.filter((v) => (now - v.t0) * boost < duration);
|
|
2225
2268
|
const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2269
|
+
const rect = content.getBoundingClientRect();
|
|
2270
|
+
const band = this.visibleBand(content, rect);
|
|
2271
|
+
const w = rect.width;
|
|
2272
|
+
const h = band.height;
|
|
2273
|
+
canvas.style.top = `${band.top - rect.top}px`;
|
|
2274
|
+
canvas.style.height = `${h}px`;
|
|
2228
2275
|
if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
|
|
2229
2276
|
canvas.width = Math.round(w * dpr);
|
|
2230
2277
|
canvas.height = Math.round(h * dpr);
|
|
@@ -2232,16 +2279,21 @@ var FadePainter = class {
|
|
|
2232
2279
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2233
2280
|
ctx.clearRect(0, 0, w, h);
|
|
2234
2281
|
if (this.veils.length > 0) {
|
|
2235
|
-
const origin =
|
|
2282
|
+
const origin = {
|
|
2283
|
+
left: rect.left,
|
|
2284
|
+
top: band.top
|
|
2285
|
+
};
|
|
2236
2286
|
const bgCache = /* @__PURE__ */ new Map();
|
|
2237
2287
|
const groups = /* @__PURE__ */ new Map();
|
|
2238
2288
|
const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
|
|
2239
2289
|
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
|
|
2240
|
-
|
|
2241
|
-
let node = walker.
|
|
2242
|
-
|
|
2290
|
+
walker.currentNode = content;
|
|
2291
|
+
let node = walker.lastChild();
|
|
2292
|
+
let end = this.length;
|
|
2293
|
+
while (node != null && end > minStart) {
|
|
2243
2294
|
const len = node.data.length;
|
|
2244
|
-
|
|
2295
|
+
const base = end - len;
|
|
2296
|
+
if (len > 0) for (let vi = 0; vi < this.veils.length; vi++) {
|
|
2245
2297
|
const v = this.veils[vi];
|
|
2246
2298
|
const s = Math.max(v.start - base, 0);
|
|
2247
2299
|
const e = Math.min(v.end - base, len);
|
|
@@ -2265,8 +2317,8 @@ var FadePainter = class {
|
|
|
2265
2317
|
range.setEnd(node, e);
|
|
2266
2318
|
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);
|
|
2267
2319
|
}
|
|
2268
|
-
|
|
2269
|
-
node = walker.
|
|
2320
|
+
end = base;
|
|
2321
|
+
node = walker.previousNode();
|
|
2270
2322
|
}
|
|
2271
2323
|
for (const group of groups.values()) {
|
|
2272
2324
|
ctx.globalAlpha = group.alpha;
|
|
@@ -2299,9 +2351,10 @@ function renderFadeMarkdown(props) {
|
|
|
2299
2351
|
"aria-hidden": true,
|
|
2300
2352
|
style: {
|
|
2301
2353
|
position: "absolute",
|
|
2302
|
-
|
|
2354
|
+
left: 0,
|
|
2355
|
+
top: 0,
|
|
2303
2356
|
width: "100%",
|
|
2304
|
-
height:
|
|
2357
|
+
height: 0,
|
|
2305
2358
|
pointerEvents: "none"
|
|
2306
2359
|
}
|
|
2307
2360
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wingleeio/mugen-markdown",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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.6"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsdown",
|