@wingleeio/mugen-markdown 0.4.2 → 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 +60 -9
- package/dist/index.mjs +60 -9
- 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,7 +2280,10 @@ 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);
|
|
@@ -2302,9 +2352,10 @@ function renderFadeMarkdown(props) {
|
|
|
2302
2352
|
"aria-hidden": true,
|
|
2303
2353
|
style: {
|
|
2304
2354
|
position: "absolute",
|
|
2305
|
-
|
|
2355
|
+
left: 0,
|
|
2356
|
+
top: 0,
|
|
2306
2357
|
width: "100%",
|
|
2307
|
-
height:
|
|
2358
|
+
height: 0,
|
|
2308
2359
|
pointerEvents: "none"
|
|
2309
2360
|
}
|
|
2310
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,7 +2279,10 @@ 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);
|
|
@@ -2301,9 +2351,10 @@ function renderFadeMarkdown(props) {
|
|
|
2301
2351
|
"aria-hidden": true,
|
|
2302
2352
|
style: {
|
|
2303
2353
|
position: "absolute",
|
|
2304
|
-
|
|
2354
|
+
left: 0,
|
|
2355
|
+
top: 0,
|
|
2305
2356
|
width: "100%",
|
|
2306
|
-
height:
|
|
2357
|
+
height: 0,
|
|
2307
2358
|
pointerEvents: "none"
|
|
2308
2359
|
}
|
|
2309
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",
|