@wingleeio/mugen-markdown 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2050,25 +2050,42 @@ const EMA_SEED_MS = 160;
2050
2050
  const MIN_FADE_MS = 120;
2051
2051
  const MAX_FADE_MS = 400;
2052
2052
  const MAX_VEILS = 32;
2053
- function commonPrefixLength(a, b) {
2054
- const n = Math.min(a.length, b.length);
2055
- let i = 0;
2056
- while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
2057
- return i;
2058
- }
2059
2053
  function inChrome(node, container) {
2060
2054
  for (let p = node.parentElement; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
2061
2055
  return false;
2062
2056
  }
2057
+ /** Whether `el` (or an ancestor up to `container`) is interactive chrome. */
2058
+ function elementInChrome(el, container) {
2059
+ for (let p = el; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
2060
+ return false;
2061
+ }
2063
2062
  function contentTextFilter(container) {
2064
2063
  return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
2065
2064
  }
2066
- /** `container.textContent`, minus interactive chrome (see {@link inChrome}). */
2067
- function contentText(container) {
2065
+ /** Chrome-free text length of `container` a full walk; used to seed/reconcile. */
2066
+ function chromeFreeLength(container) {
2068
2067
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, contentTextFilter(container));
2069
- let s = "";
2070
- for (let n = walker.nextNode(); n != null; n = walker.nextNode()) s += n.data;
2071
- return s;
2068
+ let n = 0;
2069
+ for (let t = walker.nextNode(); t != null; t = walker.nextNode()) n += t.data.length;
2070
+ return n;
2071
+ }
2072
+ /**
2073
+ * Chrome-free text length of one node's subtree — used to fold a single
2074
+ * added/removed node into the running length without touching the rest of the
2075
+ * DOM. Buttons inside the node are skipped; a button node itself counts zero.
2076
+ */
2077
+ function subtreeTextLength(node) {
2078
+ if (node.nodeType === 3) return node.data.length;
2079
+ if (node.nodeType !== 1) return 0;
2080
+ const el = node;
2081
+ if (el.tagName === "BUTTON") return 0;
2082
+ let n = 0;
2083
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { acceptNode: (t) => {
2084
+ for (let p = t.parentElement; p != null && p !== el; p = p.parentElement) if (p.tagName === "BUTTON") return NodeFilter.FILTER_REJECT;
2085
+ return NodeFilter.FILTER_ACCEPT;
2086
+ } });
2087
+ for (let t = walker.nextNode(); t != null; t = walker.nextNode()) n += t.data.length;
2088
+ return n;
2072
2089
  }
2073
2090
  let scratch;
2074
2091
  function scratchCtx() {
@@ -2110,14 +2127,20 @@ function effectiveBackground(el, cache) {
2110
2127
  }
2111
2128
  /**
2112
2129
  * Paints the dissolving veil over a single content element's newly-arrived text.
2113
- * Driven by a MutationObserver: a rAF loop runs only while veils are alive, then
2114
- * stops until the next mutation, so an idle (settled) block uses no frames.
2130
+ * Driven by a MutationObserver that folds each change into a running length, so
2131
+ * a streaming tick costs O(delta) never an O(n) walk of the whole content,
2132
+ * which is what made long streams lag. A rAF loop runs only while veils are
2133
+ * alive, then stops until the next mutation, so an idle (settled) block uses no
2134
+ * frames. Only the small, recent veil region is ever measured for geometry.
2115
2135
  */
2116
2136
  var FadePainter = class {
2117
2137
  content = null;
2118
2138
  canvas = null;
2119
2139
  ctx = null;
2120
- prevText = "";
2140
+ /** Chrome-free text length, tracked incrementally from mutation records. */
2141
+ length = 0;
2142
+ /** Net length change observed since the last frame folded it in. */
2143
+ pendingDelta = 0;
2121
2144
  veils = [];
2122
2145
  ema = EMA_SEED_MS;
2123
2146
  lastAppend = 0;
@@ -2131,14 +2154,29 @@ var FadePainter = class {
2131
2154
  this.content = content;
2132
2155
  this.canvas = canvas;
2133
2156
  this.ctx = ctx;
2134
- this.prevText = contentText(content);
2135
- this.mo = new MutationObserver(() => this.wake());
2157
+ this.length = chromeFreeLength(content);
2158
+ this.mo = new MutationObserver((records) => this.onMutations(records));
2136
2159
  this.mo.observe(content, {
2137
2160
  subtree: true,
2138
2161
  childList: true,
2139
- characterData: true
2162
+ characterData: true,
2163
+ characterDataOldValue: true
2140
2164
  });
2141
2165
  }
2166
+ onMutations(records) {
2167
+ const content = this.content;
2168
+ if (content == null) return;
2169
+ for (const rec of records) if (rec.type === "characterData") {
2170
+ const node = rec.target;
2171
+ if (inChrome(node, content)) continue;
2172
+ this.pendingDelta += (node.nodeValue?.length ?? 0) - (rec.oldValue?.length ?? 0);
2173
+ } else if (rec.type === "childList") {
2174
+ if (rec.target instanceof Element && elementInChrome(rec.target, content)) continue;
2175
+ for (let i = 0; i < rec.addedNodes.length; i++) this.pendingDelta += subtreeTextLength(rec.addedNodes[i]);
2176
+ for (let i = 0; i < rec.removedNodes.length; i++) this.pendingDelta -= subtreeTextLength(rec.removedNodes[i]);
2177
+ }
2178
+ this.wake();
2179
+ }
2142
2180
  destroy() {
2143
2181
  this.mo?.disconnect();
2144
2182
  this.mo = null;
@@ -2163,25 +2201,24 @@ var FadePainter = class {
2163
2201
  return;
2164
2202
  }
2165
2203
  const now = performance.now();
2166
- const text = contentText(content);
2167
- if (text !== this.prevText) {
2168
- const prefix = commonPrefixLength(this.prevText, text);
2169
- this.veils = this.veils.map((v) => ({
2170
- ...v,
2171
- start: Math.min(v.start, prefix),
2172
- end: Math.min(v.end, prefix)
2173
- })).filter((v) => v.end > v.start);
2174
- if (text.length > prefix) {
2204
+ if (this.pendingDelta !== 0) {
2205
+ const newLength = Math.max(0, this.length + this.pendingDelta);
2206
+ this.pendingDelta = 0;
2207
+ if (newLength > this.length) {
2175
2208
  if (this.lastAppend > 0) this.ema = this.ema * .7 + Math.min(now - this.lastAppend, 1e3) * .3;
2176
2209
  this.lastAppend = now;
2177
2210
  this.veils.push({
2178
- start: prefix,
2179
- end: text.length,
2211
+ start: this.length,
2212
+ end: newLength,
2180
2213
  t0: now
2181
2214
  });
2182
2215
  if (this.veils.length > MAX_VEILS) this.veils.splice(0, this.veils.length - MAX_VEILS);
2183
- }
2184
- this.prevText = text;
2216
+ } else if (newLength < this.length) this.veils = this.veils.map((v) => ({
2217
+ ...v,
2218
+ start: Math.min(v.start, newLength),
2219
+ end: Math.min(v.end, newLength)
2220
+ })).filter((v) => v.end > v.start);
2221
+ this.length = newLength;
2185
2222
  }
2186
2223
  const duration = Math.min(MAX_FADE_MS, Math.max(MIN_FADE_MS, this.ema * 3));
2187
2224
  const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
@@ -2201,11 +2238,13 @@ var FadePainter = class {
2201
2238
  const groups = /* @__PURE__ */ new Map();
2202
2239
  const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
2203
2240
  const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
2204
- let base = 0;
2205
- let node = walker.nextNode();
2206
- while (node != null) {
2241
+ walker.currentNode = content;
2242
+ let node = walker.lastChild();
2243
+ let end = this.length;
2244
+ while (node != null && end > minStart) {
2207
2245
  const len = node.data.length;
2208
- if (len > 0 && base + len > minStart) for (let vi = 0; vi < this.veils.length; vi++) {
2246
+ const base = end - len;
2247
+ if (len > 0) for (let vi = 0; vi < this.veils.length; vi++) {
2209
2248
  const v = this.veils[vi];
2210
2249
  const s = Math.max(v.start - base, 0);
2211
2250
  const e = Math.min(v.end - base, len);
@@ -2229,8 +2268,8 @@ var FadePainter = class {
2229
2268
  range.setEnd(node, e);
2230
2269
  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);
2231
2270
  }
2232
- base += len;
2233
- node = walker.nextNode();
2271
+ end = base;
2272
+ node = walker.previousNode();
2234
2273
  }
2235
2274
  for (const group of groups.values()) {
2236
2275
  ctx.globalAlpha = group.alpha;
@@ -2241,6 +2280,7 @@ var FadePainter = class {
2241
2280
  }
2242
2281
  if (this.veils.length > 0) this.raf = requestAnimationFrame(this.frame);
2243
2282
  else {
2283
+ this.length = chromeFreeLength(content);
2244
2284
  this.running = false;
2245
2285
  this.raf = 0;
2246
2286
  }
package/dist/index.mjs CHANGED
@@ -2049,25 +2049,42 @@ const EMA_SEED_MS = 160;
2049
2049
  const MIN_FADE_MS = 120;
2050
2050
  const MAX_FADE_MS = 400;
2051
2051
  const MAX_VEILS = 32;
2052
- function commonPrefixLength(a, b) {
2053
- const n = Math.min(a.length, b.length);
2054
- let i = 0;
2055
- while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
2056
- return i;
2057
- }
2058
2052
  function inChrome(node, container) {
2059
2053
  for (let p = node.parentElement; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
2060
2054
  return false;
2061
2055
  }
2056
+ /** Whether `el` (or an ancestor up to `container`) is interactive chrome. */
2057
+ function elementInChrome(el, container) {
2058
+ for (let p = el; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
2059
+ return false;
2060
+ }
2062
2061
  function contentTextFilter(container) {
2063
2062
  return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
2064
2063
  }
2065
- /** `container.textContent`, minus interactive chrome (see {@link inChrome}). */
2066
- function contentText(container) {
2064
+ /** Chrome-free text length of `container` a full walk; used to seed/reconcile. */
2065
+ function chromeFreeLength(container) {
2067
2066
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, contentTextFilter(container));
2068
- let s = "";
2069
- for (let n = walker.nextNode(); n != null; n = walker.nextNode()) s += n.data;
2070
- return s;
2067
+ let n = 0;
2068
+ for (let t = walker.nextNode(); t != null; t = walker.nextNode()) n += t.data.length;
2069
+ return n;
2070
+ }
2071
+ /**
2072
+ * Chrome-free text length of one node's subtree — used to fold a single
2073
+ * added/removed node into the running length without touching the rest of the
2074
+ * DOM. Buttons inside the node are skipped; a button node itself counts zero.
2075
+ */
2076
+ function subtreeTextLength(node) {
2077
+ if (node.nodeType === 3) return node.data.length;
2078
+ if (node.nodeType !== 1) return 0;
2079
+ const el = node;
2080
+ if (el.tagName === "BUTTON") return 0;
2081
+ let n = 0;
2082
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { acceptNode: (t) => {
2083
+ for (let p = t.parentElement; p != null && p !== el; p = p.parentElement) if (p.tagName === "BUTTON") return NodeFilter.FILTER_REJECT;
2084
+ return NodeFilter.FILTER_ACCEPT;
2085
+ } });
2086
+ for (let t = walker.nextNode(); t != null; t = walker.nextNode()) n += t.data.length;
2087
+ return n;
2071
2088
  }
2072
2089
  let scratch;
2073
2090
  function scratchCtx() {
@@ -2109,14 +2126,20 @@ function effectiveBackground(el, cache) {
2109
2126
  }
2110
2127
  /**
2111
2128
  * Paints the dissolving veil over a single content element's newly-arrived text.
2112
- * Driven by a MutationObserver: a rAF loop runs only while veils are alive, then
2113
- * stops until the next mutation, so an idle (settled) block uses no frames.
2129
+ * Driven by a MutationObserver that folds each change into a running length, so
2130
+ * a streaming tick costs O(delta) never an O(n) walk of the whole content,
2131
+ * which is what made long streams lag. A rAF loop runs only while veils are
2132
+ * alive, then stops until the next mutation, so an idle (settled) block uses no
2133
+ * frames. Only the small, recent veil region is ever measured for geometry.
2114
2134
  */
2115
2135
  var FadePainter = class {
2116
2136
  content = null;
2117
2137
  canvas = null;
2118
2138
  ctx = null;
2119
- prevText = "";
2139
+ /** Chrome-free text length, tracked incrementally from mutation records. */
2140
+ length = 0;
2141
+ /** Net length change observed since the last frame folded it in. */
2142
+ pendingDelta = 0;
2120
2143
  veils = [];
2121
2144
  ema = EMA_SEED_MS;
2122
2145
  lastAppend = 0;
@@ -2130,14 +2153,29 @@ var FadePainter = class {
2130
2153
  this.content = content;
2131
2154
  this.canvas = canvas;
2132
2155
  this.ctx = ctx;
2133
- this.prevText = contentText(content);
2134
- this.mo = new MutationObserver(() => this.wake());
2156
+ this.length = chromeFreeLength(content);
2157
+ this.mo = new MutationObserver((records) => this.onMutations(records));
2135
2158
  this.mo.observe(content, {
2136
2159
  subtree: true,
2137
2160
  childList: true,
2138
- characterData: true
2161
+ characterData: true,
2162
+ characterDataOldValue: true
2139
2163
  });
2140
2164
  }
2165
+ onMutations(records) {
2166
+ const content = this.content;
2167
+ if (content == null) return;
2168
+ for (const rec of records) if (rec.type === "characterData") {
2169
+ const node = rec.target;
2170
+ if (inChrome(node, content)) continue;
2171
+ this.pendingDelta += (node.nodeValue?.length ?? 0) - (rec.oldValue?.length ?? 0);
2172
+ } else if (rec.type === "childList") {
2173
+ if (rec.target instanceof Element && elementInChrome(rec.target, content)) continue;
2174
+ for (let i = 0; i < rec.addedNodes.length; i++) this.pendingDelta += subtreeTextLength(rec.addedNodes[i]);
2175
+ for (let i = 0; i < rec.removedNodes.length; i++) this.pendingDelta -= subtreeTextLength(rec.removedNodes[i]);
2176
+ }
2177
+ this.wake();
2178
+ }
2141
2179
  destroy() {
2142
2180
  this.mo?.disconnect();
2143
2181
  this.mo = null;
@@ -2162,25 +2200,24 @@ var FadePainter = class {
2162
2200
  return;
2163
2201
  }
2164
2202
  const now = performance.now();
2165
- const text = contentText(content);
2166
- if (text !== this.prevText) {
2167
- const prefix = commonPrefixLength(this.prevText, text);
2168
- this.veils = this.veils.map((v) => ({
2169
- ...v,
2170
- start: Math.min(v.start, prefix),
2171
- end: Math.min(v.end, prefix)
2172
- })).filter((v) => v.end > v.start);
2173
- if (text.length > prefix) {
2203
+ if (this.pendingDelta !== 0) {
2204
+ const newLength = Math.max(0, this.length + this.pendingDelta);
2205
+ this.pendingDelta = 0;
2206
+ if (newLength > this.length) {
2174
2207
  if (this.lastAppend > 0) this.ema = this.ema * .7 + Math.min(now - this.lastAppend, 1e3) * .3;
2175
2208
  this.lastAppend = now;
2176
2209
  this.veils.push({
2177
- start: prefix,
2178
- end: text.length,
2210
+ start: this.length,
2211
+ end: newLength,
2179
2212
  t0: now
2180
2213
  });
2181
2214
  if (this.veils.length > MAX_VEILS) this.veils.splice(0, this.veils.length - MAX_VEILS);
2182
- }
2183
- this.prevText = text;
2215
+ } else if (newLength < this.length) this.veils = this.veils.map((v) => ({
2216
+ ...v,
2217
+ start: Math.min(v.start, newLength),
2218
+ end: Math.min(v.end, newLength)
2219
+ })).filter((v) => v.end > v.start);
2220
+ this.length = newLength;
2184
2221
  }
2185
2222
  const duration = Math.min(MAX_FADE_MS, Math.max(MIN_FADE_MS, this.ema * 3));
2186
2223
  const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
@@ -2200,11 +2237,13 @@ var FadePainter = class {
2200
2237
  const groups = /* @__PURE__ */ new Map();
2201
2238
  const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
2202
2239
  const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
2203
- let base = 0;
2204
- let node = walker.nextNode();
2205
- while (node != null) {
2240
+ walker.currentNode = content;
2241
+ let node = walker.lastChild();
2242
+ let end = this.length;
2243
+ while (node != null && end > minStart) {
2206
2244
  const len = node.data.length;
2207
- if (len > 0 && base + len > minStart) for (let vi = 0; vi < this.veils.length; vi++) {
2245
+ const base = end - len;
2246
+ if (len > 0) for (let vi = 0; vi < this.veils.length; vi++) {
2208
2247
  const v = this.veils[vi];
2209
2248
  const s = Math.max(v.start - base, 0);
2210
2249
  const e = Math.min(v.end - base, len);
@@ -2228,8 +2267,8 @@ var FadePainter = class {
2228
2267
  range.setEnd(node, e);
2229
2268
  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);
2230
2269
  }
2231
- base += len;
2232
- node = walker.nextNode();
2270
+ end = base;
2271
+ node = walker.previousNode();
2233
2272
  }
2234
2273
  for (const group of groups.values()) {
2235
2274
  ctx.globalAlpha = group.alpha;
@@ -2240,6 +2279,7 @@ var FadePainter = class {
2240
2279
  }
2241
2280
  if (this.veils.length > 0) this.raf = requestAnimationFrame(this.frame);
2242
2281
  else {
2282
+ this.length = chromeFreeLength(content);
2243
2283
  this.running = false;
2244
2284
  this.raf = 0;
2245
2285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wingleeio/mugen-markdown",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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.4"
68
+ "@wingleeio/mugen": "0.3.5"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsdown",