@miaskiewicz/turbo-dom 0.5.7 → 0.6.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.
@@ -16,6 +16,7 @@ import { matchesSelector, querySelector as qsel, querySelectorAll as qselAll } f
16
16
  import { serializeInner, serializeOuter } from './html-serialize.mjs';
17
17
  import { CSSStyleSheet, styleSheetList } from './cssom.mjs';
18
18
  import { COLOR_PROPS, canonicalizeColor } from './color.mjs';
19
+ import { blockText, canonical, declarations, expand, longhandAlias, longhands, longhandValue, shorthandValue } from './css-shorthand.mjs';
19
20
  import { SVGAnimatedString, SVGAnimatedLength, SVGAnimatedRect, SVG_LENGTH_ATTRS } from './svg.mjs';
20
21
 
21
22
  // Per-node query-result cache keyed by (selector, document version). querySelectorAll
@@ -58,6 +59,22 @@ function cachedQS(node, sel) {
58
59
  }
59
60
  import { Buffer } from './buffer.mjs';
60
61
  import { makeCanvasStub } from './stubs.mjs';
62
+ import {
63
+ constructCustomElement, createCustomElement, clonedElement, insertedNode, parsedFragment, removedNode,
64
+ attributeChanged, adoptedNode, freezeParsedIs, parsedIsAttribute, reactionDepth, withReactions,
65
+ } from './custom-elements.mjs';
66
+
67
+ // Custom element reactions ([CEReactions], custom-elements.mjs): a DOM call that can queue
68
+ // reactions re-enters itself inside one reaction scope when `doc` has the hooks armed and no
69
+ // scope is open yet, so its reactions run when it returns. One predicted-false read
70
+ // otherwise. `METHOD` holds the original implementations (spy-proof re-entry).
71
+ const ceScope = (doc) => doc.__ceRegistry !== undefined && reactionDepth === 0;
72
+ const METHOD = {};
73
+
74
+ // DOM versions come from ONE process-wide counter, so a version value is never reused by
75
+ // another document: a node adopted into another document cannot hit a cache entry (keyed on
76
+ // its old document's version) by coincidence (miaskiewicz/turbo-dom#29 item 2).
77
+ let VERSION = 0;
61
78
 
62
79
  export const ELEMENT_NODE = 1;
63
80
  export const TEXT_NODE = 3;
@@ -69,7 +86,62 @@ export const DOCUMENT_FRAGMENT_NODE = 11;
69
86
  const SVG_NS = 'http://www.w3.org/2000/svg';
70
87
  const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
71
88
  const HTML_NS = 'http://www.w3.org/1999/xhtml';
72
- const nsUri = (short) => (short === 'svg' ? SVG_NS : short === 'math' ? MATHML_NS : HTML_NS);
89
+ const XML_NS = 'http://www.w3.org/XML/1998/namespace';
90
+ const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
91
+ // An element's `__ns` is '' (HTML), 'svg' or 'math' — what the parser produces — or, for
92
+ // createElementNS in any other namespace, 'ns:' + the namespace URI ('ns:' alone is the
93
+ // null namespace). Every '' / truthy test ("HTML or not") stays a single string check.
94
+ const nsUri = (short) => (short === '' ? HTML_NS : short === 'svg' ? SVG_NS : short === 'math' ? MATHML_NS
95
+ : short.length > 3 ? short.slice(3) : null);
96
+ const nsShort = (uri) => (uri === HTML_NS ? '' : uri === SVG_NS ? 'svg' : uri === MATHML_NS ? 'math' : 'ns:' + (uri ?? ''));
97
+
98
+ // A DOMException (a plain Error with its name where the host has none, e.g. a bare V8).
99
+ function domError(name, message) {
100
+ if (typeof DOMException === 'function') return new DOMException(message, name);
101
+ const error = new Error(message);
102
+ error.name = name;
103
+ return error;
104
+ }
105
+ // DOM pre-insert / replace validity step 2: `node` must not be a host-including inclusive
106
+ // ancestor of `parent` (inserting a node into its own subtree would build a parent cycle).
107
+ function assertNotAncestor(parent, node) {
108
+ for (let n = parent; n; n = n.parentNode || (n.__isShadowRoot ? n.host : null)) {
109
+ if (n === node) throw domError('HierarchyRequestError', 'The new child element contains the parent.');
110
+ }
111
+ }
112
+ // DOM "valid element local name" (createElement / createElementNS, Chrome 156): an ASCII
113
+ // letter followed by anything but ASCII whitespace, NULL, '/' and '>'; or ':', '_' or a
114
+ // non-ASCII code point followed by ASCII alphanumerics, '-', '.', ':', '_' and non-ASCII.
115
+ function validLocalName(name) {
116
+ const n = name.length;
117
+ if (n === 0) return false;
118
+ const c0 = name.charCodeAt(0) | 0x20;
119
+ if (c0 >= 97 && c0 <= 122) {
120
+ for (let i = 1; i < n; i++) {
121
+ const c = name.charCodeAt(i);
122
+ if (c <= 32 ? c === 32 || c === 9 || c === 10 || c === 12 || c === 13 || c === 0 : c === 47 || c === 62) return false;
123
+ }
124
+ return true;
125
+ }
126
+ const first = name.charCodeAt(0);
127
+ if (first !== 58 && first !== 95 && first < 0x80) return false;
128
+ for (let i = 1; i < n; i++) {
129
+ const c = name.charCodeAt(i);
130
+ if (c >= 0x80 || c === 45 || c === 46 || c === 58 || c === 95 || (c >= 48 && c <= 57) || ((c | 0x20) >= 97 && (c | 0x20) <= 122)) continue;
131
+ return false;
132
+ }
133
+ return true;
134
+ }
135
+ // DOM "valid namespace prefix": non-empty, no ASCII whitespace, NULL, '/' or '>'.
136
+ function validPrefix(prefix) {
137
+ if (prefix.length === 0) return false;
138
+ for (let i = 0; i < prefix.length; i++) {
139
+ const c = prefix.charCodeAt(i);
140
+ if (c === 32 || c === 9 || c === 10 || c === 12 || c === 13 || c === 0 || c === 47 || c === 62) return false;
141
+ }
142
+ return true;
143
+ }
144
+ const invalidName = (name) => domError('InvalidCharacterError', `The qualified name provided ('${name}') contains the invalid name-start character or invalid character.`);
73
145
 
74
146
  // ------------------------------------------------------------------ Node ----
75
147
  export class Node extends EventTarget {
@@ -140,35 +212,57 @@ export class Node extends EventTarget {
140
212
  appendChild(node) { return this.insertBefore(node, null); }
141
213
 
142
214
  insertBefore(node, ref) {
143
- if (node.nodeType === DOCUMENT_FRAGMENT_NODE) {
144
- for (const c of node.__children().slice()) this.insertBefore(c, ref);
145
- return node;
146
- }
215
+ if ((this.ownerDocument.__ceRegistry !== undefined || node.ownerDocument.__ceRegistry !== undefined) && reactionDepth === 0) return withReactions(METHOD.insertBefore, this, [node, ref]);
216
+ // DOM "pre-insert": check the reference child before touching the tree, and a node
217
+ // inserted before itself goes before its next sibling (miaskiewicz/turbo-dom#34)
218
+ assertNotAncestor(this, node);
219
+ if (ref && ref.parentNode !== this) throw new Error('NotFoundError: ref is not a child');
220
+ if (ref === node) ref = node.nextSibling;
221
+ if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { insertBatch(this, [node], ref, EMPTY_NODES); return node; }
147
222
  if (node.parentNode) node.parentNode.removeChild(node);
148
223
  const kids = this.__children();
149
224
  const i = ref ? kids.indexOf(ref) : -1;
150
- if (ref && i === -1) throw new Error('NotFoundError: ref is not a child');
225
+ // a node from another document is adopted first (DOM "insert" step 7.1)
226
+ if (node.ownerDocument !== this.ownerDocument) adopt(node, this.ownerDocument);
227
+ if (liveRanges.length !== 0) liveInsert(this, ref ? i : kids.length, 1);
151
228
  if (ref) kids.splice(i, 0, node); else kids.push(node);
152
229
  node.parentNode = this;
153
- node.ownerDocument = this.ownerDocument;
154
- notifyMutation(this, 'childList', node, null, ref || null);
230
+ if (moQuiet === 0) notifyMutation(this, 'childList', node, null, ref || null, null, null, ref ? kids[i - 1] ?? null : kids[kids.length - 2] ?? null);
231
+ else this.__touch();
232
+ // custom elements (custom-elements.mjs): upgrades + connectedCallback, only once a
233
+ // registry exists — one predicted-false read otherwise
234
+ const doc = this.ownerDocument;
235
+ if (doc !== null && doc.__ceRegistry !== undefined) insertedNode(node);
155
236
  return node;
156
237
  }
157
238
 
158
239
  removeChild(node) {
240
+ if (this.ownerDocument.__ceRegistry !== undefined && reactionDepth === 0) return withReactions(METHOD.removeChild, this, [node]);
159
241
  const kids = this.__children();
160
242
  const i = kids.indexOf(node);
161
243
  if (i === -1) throw new Error('NotFoundError: node is not a child');
162
- const next = kids[i + 1] || null;
244
+ const next = kids[i + 1] || null, previous = kids[i - 1] ?? null;
245
+ if (liveRanges.length !== 0) liveRemove(this, node, i);
246
+ const doc = this.ownerDocument, ce = doc !== null && doc.__ceRegistry !== undefined && this.isConnected;
163
247
  kids.splice(i, 1);
164
248
  node.parentNode = null;
165
- notifyMutation(this, 'childList', null, node, next);
249
+ if (moQuiet === 0) notifyMutation(this, 'childList', null, node, next, null, null, previous);
250
+ else this.__touch();
251
+ if (ce) removedNode(node);
166
252
  return node;
167
253
  }
168
254
 
255
+ // DOM "replace": remove the old child, then insert the new one before the old child's
256
+ // next sibling (or the new node's own next sibling when that is it) — so replacing a
257
+ // node with itself leaves it in place (miaskiewicz/turbo-dom#34)
169
258
  replaceChild(newNode, oldNode) {
170
- this.insertBefore(newNode, oldNode);
259
+ if ((this.ownerDocument.__ceRegistry !== undefined || newNode.ownerDocument.__ceRegistry !== undefined) && reactionDepth === 0) return withReactions(METHOD.replaceChild, this, [newNode, oldNode]);
260
+ assertNotAncestor(this, newNode);
261
+ if (oldNode.parentNode !== this) throw new Error('NotFoundError: node is not a child');
262
+ let ref = oldNode.nextSibling;
263
+ if (ref === newNode) ref = newNode.nextSibling;
171
264
  this.removeChild(oldNode);
265
+ this.insertBefore(newNode, ref);
172
266
  return oldNode;
173
267
  }
174
268
 
@@ -203,23 +297,31 @@ export class Node extends EventTarget {
203
297
  while (n.parentNode) n = n.parentNode;
204
298
  return n;
205
299
  }
300
+ // DOM normalize: an empty Text is removed; a Text takes the data of the Text siblings
301
+ // after it (one characterData change), which are then removed (childList records).
206
302
  normalize() {
207
303
  const kids = this.__children();
208
- let changed = false;
209
- for (let i = kids.length - 1; i > 0; i--) {
210
- if (kids[i].nodeType === TEXT_NODE && kids[i - 1].nodeType === TEXT_NODE) {
211
- kids[i - 1]._data += kids[i].data; kids[i].parentNode = null; kids.splice(i, 1); changed = true;
212
- }
304
+ for (let i = 0; i < kids.length;) {
305
+ const k = kids[i];
306
+ if (k.nodeType === TEXT_NODE) {
307
+ if (k.data === '') { this.removeChild(k); continue; }
308
+ let more = '';
309
+ for (let j = i + 1; j < kids.length && kids[j].nodeType === TEXT_NODE; j++) more += kids[j].data;
310
+ const length = k.length;
311
+ if (more !== '') k.__replace(length, 0, more);
312
+ if (liveRanges.length !== 0) liveMerge(this, k, i, length);
313
+ while (i + 1 < kids.length && kids[i + 1].nodeType === TEXT_NODE) this.removeChild(kids[i + 1]);
314
+ } else if (k.nodeType === ELEMENT_NODE) k.normalize();
315
+ i++;
213
316
  }
214
- for (let i = kids.length - 1; i >= 0; i--) {
215
- if (kids[i].nodeType === TEXT_NODE && kids[i].data === '') { kids[i].parentNode = null; kids.splice(i, 1); changed = true; }
216
- else if (kids[i].nodeType === ELEMENT_NODE) kids[i].normalize();
217
- }
218
- // normalize mutates __kids in place — bump __version (invalidate cached queries)
219
- // and feed MutationObservers, like every other childList mutation path.
220
- if (changed) notifyMutation(this, 'childList', null, null, null);
221
317
  }
222
- replaceChildren(...nodes) { this.__detachKids(); this.__kids = []; this.__touch(); for (const n of nodes) this.appendChild(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n); }
318
+ replaceChildren(...nodes) {
319
+ if (ceScope(this.ownerDocument)) return withReactions(METHOD.replaceChildren, this, nodes);
320
+ for (const n of nodes) assertNotAncestor(this, n);
321
+ const doc = this.ownerDocument, added = nodes.map((n) => toNode(doc, n));
322
+ if (observed(this)) { insertBatch(this, added, null, this.__children().slice()); return; }
323
+ this.__detachKids(); this.__kids = []; this.__touch(); for (const n of added) this.appendChild(n);
324
+ }
223
325
  // bitmask: 1 DISCONNECTED, 2 PRECEDING, 4 FOLLOWING, 8 CONTAINS, 16 CONTAINED_BY
224
326
  compareDocumentPosition(other) {
225
327
  if (other === this) return 0;
@@ -233,11 +335,11 @@ export class Node extends EventTarget {
233
335
  if (a === -1 || b === -1) return 1;
234
336
  return a < b ? 4 : 2;
235
337
  }
338
+ // cloneNode = DOM "clone a node" into the node's own document; importNode clones into
339
+ // another one. Every node type implements `__clone(document, deep)`.
236
340
  cloneNode(deep = false) {
237
- // base fallback; Element/Text/Comment override
238
- const n = new this.constructor(this.ownerDocument);
239
- if (deep) for (const c of this.__children()) n.appendChild(c.cloneNode(true));
240
- return n;
341
+ if (ceScope(this.ownerDocument)) return withReactions(METHOD.cloneNode, this, [deep]);
342
+ return this.__clone(this.ownerDocument, !!deep);
241
343
  }
242
344
 
243
345
  get textContent() {
@@ -268,16 +370,20 @@ export class Node extends EventTarget {
268
370
  return s;
269
371
  }
270
372
  set textContent(value) {
373
+ if (ceScope(this.ownerDocument)) { withReactions(METHOD.setTextContent, this, [value]); return; }
374
+ const removed = observed(this) ? this.__children().slice() : null;
271
375
  this.__detachKids();
272
376
  this.__kids = [];
273
377
  this.__touch();
274
- if (value !== '') this.appendChild(this.ownerDocument.createTextNode(String(value)));
378
+ const text = value === '' || value == null ? null : this.ownerDocument.createTextNode(String(value));
379
+ if (text !== null) { moQuiet++; try { this.appendChild(text); } finally { moQuiet--; } }
380
+ if (removed !== null && (text !== null || removed.length !== 0)) notifyMutation(this, 'childList', text === null ? EMPTY_NODES : [text], removed, null, null, null, null);
275
381
  }
276
382
 
277
383
  // bump the document version so getElementsBy* caches invalidate after direct
278
384
  // __kids reassignments (innerHTML/textContent/replaceChildren) that bypass
279
385
  // insertBefore/removeChild.
280
- __touch() { const d = this.ownerDocument; if (d) d.__version = (d.__version || 0) + 1; }
386
+ __touch() { const d = this.ownerDocument; if (d) d.__version = ++VERSION; }
281
387
 
282
388
  // Clear the outgoing children's parentNode before a wholesale `__kids = []` rewrite
283
389
  // (innerHTML/textContent/replaceChildren) — removeChild does this one at a time, these
@@ -288,7 +394,11 @@ export class Node extends EventTarget {
288
394
  // null out parent pointers would put an allocation on the React container-clear path.
289
395
  __detachKids() {
290
396
  const kids = this.__kids;
291
- if (kids) for (let i = 0; i < kids.length; i++) kids[i].parentNode = null;
397
+ if (!kids) return;
398
+ if (liveRanges.length !== 0) liveReplaceAll(this);
399
+ const doc = this.ownerDocument, ce = doc !== null && doc.__ceRegistry !== undefined && kids.length !== 0 && this.isConnected;
400
+ for (let i = 0; i < kids.length; i++) kids[i].parentNode = null;
401
+ if (ce) for (let i = 0; i < kids.length; i++) removedNode(kids[i]);
292
402
  }
293
403
 
294
404
  // Element/Document/Fragment nodeValue is null per spec (CharacterData overrides).
@@ -326,20 +436,27 @@ function viableNextSibling(node, nodes) {
326
436
  class CharacterData extends Node {
327
437
  constructor(ownerDocument, data) { super(ownerDocument); this._data = data ?? ''; }
328
438
  get data() { return this._data; }
329
- set data(v) { const old = this._data; this._data = String(v); notifyMutation(this, 'characterData', null, null, null, null, old); }
439
+ set data(v) { this.__replace(0, this._data.length, String(v)); }
440
+ // DOM "replace data": the one path every CharacterData mutation takes, so live ranges follow.
441
+ __replace(offset, count, s) {
442
+ const old = this._data;
443
+ if (liveRanges.length !== 0) liveData(this, offset, count, s.length);
444
+ this._data = old.slice(0, offset) + s + old.slice(offset + count);
445
+ notifyMutation(this, 'characterData', null, null, null, null, old);
446
+ }
330
447
  get nodeValue() { return this._data; }
331
448
  set nodeValue(v) { this.data = v; }
332
449
  get length() { return this._data.length; }
333
450
  get textContent() { return this._data; }
334
451
  set textContent(v) { this.data = v; }
335
- substringData(offset, count) { return this._data.slice(offset, offset + count); }
336
- appendData(s) { this.data = this._data + s; }
337
- insertData(offset, s) { this.data = this._data.slice(0, offset) + s + this._data.slice(offset); }
338
- deleteData(offset, count) { this.data = this._data.slice(0, offset) + this._data.slice(offset + count); }
339
- replaceData(offset, count, s) { this.data = this._data.slice(0, offset) + s + this._data.slice(offset + count); }
340
- before(...nodes) { const p = this.parentNode; if (p) for (const n of nodes) p.insertBefore(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n, this); }
341
- after(...nodes) { const p = this.parentNode; if (!p) return; const ref = viableNextSibling(this, nodes); for (const n of nodes) p.insertBefore(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n, ref); }
342
- replaceWith(...nodes) { const p = this.parentNode; if (!p) return; const ref = viableNextSibling(this, nodes); for (const n of nodes) if (n !== this) p.insertBefore(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n, ref); this.remove(); }
452
+ substringData(offset, count) { offset = clampData(this, offset); return this._data.substr(offset, Math.min(count >>> 0, this._data.length - offset)); }
453
+ appendData(s) { this.__replace(this._data.length, 0, String(s)); }
454
+ insertData(offset, s) { this.__replace(clampData(this, offset), 0, String(s)); }
455
+ deleteData(offset, count) { offset = clampData(this, offset); this.__replace(offset, Math.min(count >>> 0, this._data.length - offset), ''); }
456
+ replaceData(offset, count, s) { offset = clampData(this, offset); this.__replace(offset, Math.min(count >>> 0, this._data.length - offset), String(s)); }
457
+ before(...nodes) { childBefore(this, nodes); }
458
+ after(...nodes) { childAfter(this, nodes); }
459
+ replaceWith(...nodes) { childReplaceWith(this, nodes); }
343
460
  }
344
461
 
345
462
  export class Text extends CharacterData {
@@ -352,20 +469,27 @@ export class Text extends CharacterData {
352
469
  while (n && n.nodeType === TEXT_NODE) { s += n.data; n = n.nextSibling; }
353
470
  return s;
354
471
  }
472
+ // DOM "split a Text node": insert the new node, move live boundaries past the offset into
473
+ // it, then truncate this node's data.
355
474
  splitText(offset) {
475
+ offset = clampData(this, offset);
356
476
  const rest = this._data.slice(offset);
357
- this.data = this._data.slice(0, offset);
358
477
  const node = new Text(this.ownerDocument, rest);
359
- if (this.parentNode) this.parentNode.insertBefore(node, this.nextSibling);
478
+ const parent = this.parentNode;
479
+ if (parent) {
480
+ parent.insertBefore(node, this.nextSibling);
481
+ if (liveRanges.length !== 0) liveSplit(this, offset, node, parent, nodeIndex(this));
482
+ }
483
+ this.__replace(offset, this._data.length - offset, '');
360
484
  return node;
361
485
  }
362
- cloneNode() { return new Text(this.ownerDocument, this._data); }
486
+ __clone(doc) { return new Text(doc, this._data); }
363
487
  }
364
488
 
365
489
  export class Comment extends CharacterData {
366
490
  get nodeType() { return COMMENT_NODE; }
367
491
  get nodeName() { return '#comment'; }
368
- cloneNode() { return new Comment(this.ownerDocument, this._data); }
492
+ __clone(doc) { return new Comment(doc, this._data); }
369
493
  }
370
494
 
371
495
  export class DocumentType extends Node {
@@ -375,6 +499,7 @@ export class DocumentType extends Node {
375
499
  }
376
500
  get nodeType() { return DOCUMENT_TYPE_NODE; }
377
501
  get nodeName() { return this.name; }
502
+ __clone(doc) { return new DocumentType(doc, this.name, this.publicId, this.systemId); } // turbo-dom#35
378
503
  }
379
504
 
380
505
  // ------------------------------------------------------------ Element ----
@@ -400,12 +525,19 @@ class ClassList {
400
525
 
401
526
  export class Element extends Node {
402
527
  constructor(ownerDocument, localName, namespace = '') {
528
+ // HTMLElement (=== Element here) as a custom element constructor: `new C()` / an
529
+ // upgrade's super() for a defined C (custom-elements.mjs); internal construction never
530
+ // takes this branch.
531
+ if (new.target !== Element && new.target !== SVGElement && new.target !== MathMLElement) {
532
+ const custom = constructCustomElement(new.target);
533
+ if (custom !== null) return custom;
534
+ }
403
535
  super(ownerDocument);
404
536
  this.localName = localName;
405
537
  this.__ns = namespace; // '', 'svg', 'math'
406
538
  this.__attrs = undefined; // lazily built (buffer or []) on first attr touch
407
539
  this.__attrIdx = -1; // buffer index for lazy attr inflation
408
- this.content = null; // <template> content fragment
540
+ this.__content = undefined; // <template> contents (a fragment in the inert document), lazy
409
541
  this.shadowRoot = null; // open shadow root, if attached
410
542
  // Predeclare the hot lazy fields (left at their "absent" sentinel) so every
411
543
  // Element shares ONE hidden class. Without this, fields added on demand in
@@ -421,9 +553,32 @@ export class Element extends Node {
421
553
  }
422
554
 
423
555
  get nodeType() { return ELEMENT_NODE; }
424
- get tagName() { return this.__ns ? this.localName : this.localName.toUpperCase(); }
556
+ // HTML <template> contents: a fragment owned by the document's inert "template contents
557
+ // owner document" (no browsing context, no custom element registry), created on first
558
+ // read. <meta>'s `content` reflects its attribute; no other element has the property.
559
+ get content() {
560
+ if (this.__ns === '') {
561
+ if (this.localName === 'template') return this.__content ?? (this.__content = new DocumentFragment(this.ownerDocument.__templateDocument()));
562
+ if (this.localName === 'meta') return this.getAttribute('content') ?? '';
563
+ }
564
+ return undefined;
565
+ }
566
+ set content(value) {
567
+ if (this.__ns === '') {
568
+ if (this.localName === 'meta') { this.setAttribute('content', value); return; }
569
+ if (this.localName === 'template') return; // read-only
570
+ }
571
+ Object.defineProperty(this, 'content', { value, writable: true, enumerable: true, configurable: true });
572
+ }
573
+ // the qualified name, uppercased for HTML elements (an HTML document)
574
+ get tagName() {
575
+ const p = this.__prefix, q = p === undefined ? this.localName : `${p}:${this.localName}`;
576
+ return this.__ns ? q : q.toUpperCase();
577
+ }
425
578
  get nodeName() { return this.tagName; }
426
579
  get namespaceURI() { return nsUri(this.__ns); }
580
+ // createElementNS's prefix (`__prefix`, set only on the elements that have one)
581
+ get prefix() { return this.__prefix ?? null; }
427
582
 
428
583
  // ---- attributes ----
429
584
  // attrs inflate lazily: a buffer-backed element leaves __attrs undefined and
@@ -714,22 +869,24 @@ export class Element extends Node {
714
869
  }
715
870
 
716
871
  // ---- modern insertion ----
717
- append(...nodes) { for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
718
- prepend(...nodes) { const first = this.firstChild; for (const n of nodes) this.insertBefore(toNode(this.ownerDocument, n), first); }
719
- before(...nodes) { const p = this.parentNode; if (!p) return; for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), this); }
720
- after(...nodes) { const p = this.parentNode; if (!p) return; const ref = viableNextSibling(this, nodes); for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), ref); }
721
- replaceWith(...nodes) { const p = this.parentNode; if (!p) return; const ref = viableNextSibling(this, nodes); for (const n of nodes) if (n !== this) p.insertBefore(toNode(this.ownerDocument, n), ref); this.remove(); }
722
- replaceChildren(...nodes) { this.__detachKids(); this.__kids = []; this.__touch(); for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
872
+ append(...nodes) { parentAppend(this, nodes); }
873
+ prepend(...nodes) {
874
+ if (ceScope(this.ownerDocument)) { withReactions(METHOD.prepend, this, nodes); return; }
875
+ insertBatch(this, nodes.map((n) => toNode(this.ownerDocument, n)), viablePreviousSibling(this, null, nodes), EMPTY_NODES, true);
876
+ }
877
+ before(...nodes) { childBefore(this, nodes); }
878
+ after(...nodes) { childAfter(this, nodes); }
879
+ replaceWith(...nodes) { childReplaceWith(this, nodes); }
723
880
 
724
881
  // ---- queries ----
725
882
  matches(sel) { return matchesSelector(this, sel); }
726
- closest(sel) { let n = this; while (n && n.nodeType === ELEMENT_NODE) { if (n.matches(sel)) return n; n = n.parentNode; } return null; }
883
+ closest(sel) { let n = this; while (n && n.nodeType === ELEMENT_NODE) { if (matchesSelector(n, sel, this)) return n; n = n.parentNode; } return null; }
727
884
  querySelector(sel) { return cachedQS(this, sel); }
728
885
  querySelectorAll(sel) { return cachedQSA(this, sel); }
729
886
  getElementsByTagName(tag) {
730
- const self = this, t = tag.toLowerCase();
887
+ const self = this, raw = String(tag), t = raw.toLowerCase();
731
888
  let cv = -1, ca; // version-cache the subtree walk within this collection (live; re-walk on mutation)
732
- return liveHTMLCollection(() => { const d = self.ownerDocument, v = d ? (d.__version || 0) : 0; if (v === cv) return ca; cv = v; return (ca = collectByTag(self, t)); });
889
+ return liveHTMLCollection(() => { const d = self.ownerDocument, v = d ? (d.__version || 0) : 0; if (v === cv) return ca; cv = v; return (ca = collectByTag(self, raw, t)); });
733
890
  }
734
891
  getElementsByClassName(cls) {
735
892
  const self = this, classes = splitClasses(cls);
@@ -740,43 +897,53 @@ export class Element extends Node {
740
897
  // ---- innerHTML / outerHTML ----
741
898
  get innerHTML() { return serializeInner(this); }
742
899
  set innerHTML(html) {
743
- const frag = getParser().parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
744
- this.__detachKids();
745
- this.__kids = [];
746
- this.__touch();
747
- for (const rawChild of frag.children) {
748
- if (rawChild.nodeType === DOCUMENT_FRAGMENT_NODE && rawChild.name === 'content') continue;
749
- const child = this.ownerDocument.__inflateNested(rawChild);
750
- child.parentNode = this;
751
- this.__kids.push(child);
752
- }
900
+ if (ceScope(this.ownerDocument)) { withReactions(METHOD.setInnerHTML, this, [html]); return; }
901
+ // a <template>'s markup replaces its contents (in the inert document), not its children
902
+ const target = this.__ns === '' && this.localName === 'template' ? this.content : this;
903
+ replaceAllWithMarkup(target, this, html);
753
904
  }
754
905
  get outerHTML() { return serializeOuter(this); }
906
+ set outerHTML(html) { setOuterHTML(this, html); }
755
907
  get innerText() { return this.textContent; }
756
908
  set innerText(v) { this.textContent = v; }
757
909
 
910
+ // The markup is parsed with this element (afterbegin / beforeend) or its parent as the
911
+ // context, straight into nodes of this document (turbo-dom#39: it used to parse through a
912
+ // createElement(this.localName) scratch element — a constructed custom element, the
913
+ // namespace lost).
758
914
  insertAdjacentHTML(position, html) {
759
- const tmp = this.ownerDocument.createElement(this.localName);
760
- tmp.innerHTML = html;
761
- const nodes = tmp.__children().slice();
762
- const p = this.parentNode;
763
- switch (position) {
764
- case 'beforebegin': for (const n of nodes) p.insertBefore(n, this); break;
765
- case 'afterbegin': { const first = this.firstChild; for (const n of nodes) this.insertBefore(n, first); break; }
766
- case 'beforeend': for (const n of nodes) this.appendChild(n); break;
767
- case 'afterend': { const ref = this.nextSibling; for (const n of nodes) p.insertBefore(n, ref); break; }
768
- default: throw new Error(`bad insertAdjacentHTML position: ${position}`);
915
+ if (ceScope(this.ownerDocument)) { withReactions(METHOD.insertAdjacentHTML, this, [position, html]); return; }
916
+ const where = String(position).toLowerCase();
917
+ let parent = this, ref = null;
918
+ switch (where) {
919
+ case 'beforebegin': parent = this.parentNode; ref = this; break;
920
+ case 'afterbegin': ref = this.firstChild; break;
921
+ case 'beforeend': break;
922
+ case 'afterend': parent = this.parentNode; ref = this.nextSibling; break;
923
+ default: throw domError('SyntaxError', `The value provided ('${position}') is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'.`);
769
924
  }
925
+ if (parent === null || parent.nodeType === DOCUMENT_NODE) throw domError('NoModificationAllowedError', 'The element has no parent.');
926
+ const nodes = parseMarkup(parent.nodeType === ELEMENT_NODE ? parent : null, html, this.ownerDocument);
927
+ insertBatch(parent, nodes, ref, EMPTY_NODES);
770
928
  }
771
929
 
772
- cloneNode(deep = false) {
773
- const el = newElement(this.ownerDocument, this.localName, this.__ns);
930
+ __clone(doc, deep) {
931
+ const el = newElement(doc, this.localName, this.__ns);
932
+ if (this.__prefix !== undefined) el.__prefix = this.__prefix;
774
933
  el.__attrs = (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => ({ ...a }));
934
+ // custom element state travels (an `is` value / custom name -> "undefined", upgraded
935
+ // when the clone's document has its definition — never in a template's inert
936
+ // document); plain HTML elements and foreign ones skip the call
937
+ if (this.__ns === '' && (this.__ce !== undefined || this.__ceIs !== undefined || this.__attrIdx >= 0 || this.localName.indexOf('-') !== -1)) clonedElement(this, el);
775
938
  if (deep) {
776
939
  // build the cloned child array directly — the clone is detached, so per-child
777
940
  // appendChild's notifyMutation (version bump) + reparent check are pure waste.
778
- const src = this.__children(), od = el.ownerDocument, kids = el.__kids = new Array(src.length);
779
- for (let i = 0; i < src.length; i++) { const cl = src[i].cloneNode(true); cl.parentNode = el; cl.ownerDocument = od; kids[i] = cl; }
941
+ el.__kids = cloneKids(this, el, doc);
942
+ // HTML template "cloning steps": the contents are cloned into the copy's contents
943
+ if (this.__ns === '' && this.localName === 'template') {
944
+ const content = el.content;
945
+ content.__kids = cloneKids(this.content, content, content.ownerDocument);
946
+ }
780
947
  }
781
948
  return el;
782
949
  }
@@ -1022,10 +1189,17 @@ for (const attr of SVG_LENGTH_ATTRS) {
1022
1189
  });
1023
1190
  }
1024
1191
 
1025
- // Element factory: SVG namespace → SVGElement, everything else → Element. One place so
1192
+ // MathML-namespace elements are MathMLElements (turbo-dom#87); the interface adds no members
1193
+ // over Element here (style / dataset / focus live on Element in this runtime).
1194
+ export class MathMLElement extends Element {}
1195
+
1196
+ // createElement's ElementCreationOptions `is` (a string options argument is ignored, as Chrome).
1197
+ const isOption = (options) => (options !== null && typeof options === 'object' && options.is !== undefined ? String(options.is) : null);
1198
+
1199
+ // Element factory: SVG namespace → SVGElement, MathML → MathMLElement, everything else → Element. One place so
1026
1200
  // every construction site (buffer inflation, innerHTML, cloneNode, createElementNS) agrees.
1027
1201
  function newElement(doc, tag, ns) {
1028
- return ns === 'svg' ? new SVGElement(doc, tag, ns) : new Element(doc, tag, ns);
1202
+ return ns === 'svg' ? new SVGElement(doc, tag, ns) : ns === 'math' ? new MathMLElement(doc, tag, ns) : new Element(doc, tag, ns);
1029
1203
  }
1030
1204
 
1031
1205
 
@@ -1036,13 +1210,14 @@ export class DocumentFragment extends Node {
1036
1210
  querySelector(sel) { return cachedQS(this, sel); }
1037
1211
  querySelectorAll(sel) { return cachedQSA(this, sel); }
1038
1212
  get children() { if (this.__childrenList) return this.__childrenList; const self = this; return (this.__childrenList = liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE))); }
1039
- append(...nodes) { for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
1040
- cloneNode(deep = false) {
1041
- const f = new DocumentFragment(this.ownerDocument);
1042
- if (deep) { // direct __kids build — detached clone, per-child appendChild bookkeeping is waste (see Element.cloneNode)
1043
- const src = this.__children(), od = f.ownerDocument, kids = f.__kids = new Array(src.length);
1044
- for (let i = 0; i < src.length; i++) { const cl = src[i].cloneNode(true); cl.parentNode = f; cl.ownerDocument = od; kids[i] = cl; }
1045
- }
1213
+ // ParentNode element traversal (template content, fragments)
1214
+ get firstElementChild() { return this.__children().find((n) => n.nodeType === ELEMENT_NODE) ?? null; }
1215
+ get lastElementChild() { return this.__children().findLast((n) => n.nodeType === ELEMENT_NODE) ?? null; }
1216
+ get childElementCount() { return this.__children().filter((n) => n.nodeType === ELEMENT_NODE).length; }
1217
+ append(...nodes) { parentAppend(this, nodes); }
1218
+ __clone(doc, deep) {
1219
+ const f = new DocumentFragment(doc);
1220
+ if (deep) f.__kids = cloneKids(this, f, doc); // direct __kids build (see Element.__clone)
1046
1221
  return f;
1047
1222
  }
1048
1223
  }
@@ -1063,16 +1238,8 @@ export class ShadowRoot extends DocumentFragment {
1063
1238
  get nodeName() { return '#document-fragment'; }
1064
1239
  get innerHTML() { return serializeInner(this); }
1065
1240
  set innerHTML(html) {
1066
- const frag = getParser().parseFragment(String(html), ''); // empty context → body
1067
- this.__detachKids();
1068
- this.__kids = [];
1069
- this.__touch();
1070
- for (const rawChild of frag.children) {
1071
- if (rawChild.nodeType === DOCUMENT_FRAGMENT_NODE && rawChild.name === 'content') continue;
1072
- const child = this.ownerDocument.__inflateNested(rawChild);
1073
- child.parentNode = this;
1074
- this.__kids.push(child);
1075
- }
1241
+ if (ceScope(this.ownerDocument)) { withReactions(METHOD.setShadowInnerHTML, this, [html]); return; }
1242
+ replaceAllWithMarkup(this, null, html); // no context element → body
1076
1243
  }
1077
1244
  getElementById(id) {
1078
1245
  let found = null;
@@ -1090,9 +1257,9 @@ export class ShadowRoot extends DocumentFragment {
1090
1257
  return found;
1091
1258
  }
1092
1259
  getElementsByTagName(tag) {
1093
- const self = this, t = tag.toLowerCase();
1260
+ const self = this, raw = String(tag), t = raw.toLowerCase();
1094
1261
  let cv = -1, ca; // version-cache the subtree walk within this collection (live; re-walk on mutation)
1095
- return liveHTMLCollection(() => { const d = self.ownerDocument, v = d ? (d.__version || 0) : 0; if (v === cv) return ca; cv = v; return (ca = collectByTag(self, t)); });
1262
+ return liveHTMLCollection(() => { const d = self.ownerDocument, v = d ? (d.__version || 0) : 0; if (v === cv) return ca; cv = v; return (ca = collectByTag(self, raw, t)); });
1096
1263
  }
1097
1264
  getElementsByClassName(cls) {
1098
1265
  const self = this, classes = splitClasses(cls);
@@ -1106,6 +1273,118 @@ export class ShadowRoot extends DocumentFragment {
1106
1273
  // ------------------------------------------------------------ helpers ----
1107
1274
  function toNode(doc, n) { return typeof n === 'string' ? doc.createTextNode(n) : n; }
1108
1275
 
1276
+ // Fragment-parse `html` with `context` (an element; null = <body>) into owned nodes of
1277
+ // `doc`. Elements in other namespaces parse like <body> too.
1278
+ function parseMarkup(context, html, doc) {
1279
+ const ns = context === null ? '' : context.__ns;
1280
+ const name = context === null ? '' : ns === '' ? context.localName : ns === 'svg' || ns === 'math' ? `${ns} ${context.localName}` : '';
1281
+ const frag = getParser().parseFragment(String(html), name);
1282
+ const nodes = [];
1283
+ // (template contents never sit at the top level: they hang off their <template>)
1284
+ for (const rawChild of frag.children) nodes.push(doc.__inflateNested(rawChild));
1285
+ // custom elements: upgrades queued at creation (custom-elements.mjs parsedFragment)
1286
+ if (nodes.length !== 0 && doc.__ceRegistry !== undefined) parsedFragment(nodes);
1287
+ return nodes;
1288
+ }
1289
+
1290
+ // innerHTML: the parsed nodes replace all of `target`'s children (created first, so their
1291
+ // upgrades queue before the old children's disconnectedCallbacks, as in Chrome).
1292
+ function replaceAllWithMarkup(target, context, html) {
1293
+ const kids = parseMarkup(context, html, target.ownerDocument);
1294
+ const removed = observed(target) ? target.__children().slice() : null;
1295
+ target.__detachKids();
1296
+ for (let i = 0; i < kids.length; i++) kids[i].parentNode = target;
1297
+ target.__kids = kids;
1298
+ target.__touch();
1299
+ if (removed !== null && (kids.length !== 0 || removed.length !== 0)) notifyMutation(target, 'childList', kids.slice(), removed, null, null, null, null);
1300
+ }
1301
+
1302
+ // ParentNode append / ChildNode before / after / replaceWith, shared by elements, text,
1303
+ // comments and fragments; one reaction scope for the whole call.
1304
+ function parentAppend(parent, nodes) {
1305
+ if (ceScope(parent.ownerDocument)) { withReactions(parentAppend, null, [parent, nodes]); return; }
1306
+ insertBatch(parent, nodes.map((n) => toNode(parent.ownerDocument, n)), null, EMPTY_NODES);
1307
+ }
1308
+ function childBefore(child, nodes) {
1309
+ const p = child.parentNode;
1310
+ if (!p) return;
1311
+ if (ceScope(child.ownerDocument)) { withReactions(childBefore, null, [child, nodes]); return; }
1312
+ insertBatch(p, nodes.map((n) => toNode(child.ownerDocument, n)), viablePreviousSibling(p, child, nodes), EMPTY_NODES, true);
1313
+ }
1314
+ function childAfter(child, nodes) {
1315
+ const p = child.parentNode;
1316
+ if (!p) return;
1317
+ if (ceScope(child.ownerDocument)) { withReactions(childAfter, null, [child, nodes]); return; }
1318
+ insertBatch(p, nodes.map((n) => toNode(child.ownerDocument, n)), viableNextSibling(child, nodes), EMPTY_NODES);
1319
+ }
1320
+ // DOM "replaceWith": the node is removed before the replacements go in (a node passed among
1321
+ // its own replacements just moves; turbo-dom#40)
1322
+ function childReplaceWith(child, nodes) {
1323
+ const p = child.parentNode;
1324
+ if (!p) return;
1325
+ if (ceScope(child.ownerDocument)) { withReactions(childReplaceWith, null, [child, nodes]); return; }
1326
+ insertBatch(p, nodes.map((n) => toNode(child.ownerDocument, n)), viableNextSibling(child, nodes), nodes.includes(child) ? EMPTY_NODES : [child]);
1327
+ }
1328
+
1329
+ // outerHTML setter: the parsed markup replaces the element (no-op when detached).
1330
+ function setOuterHTML(el, html) {
1331
+ const parent = el.parentNode;
1332
+ if (parent === null) return;
1333
+ if (parent.nodeType === DOCUMENT_NODE) throw domError('NoModificationAllowedError', 'The element\'s parent is the document.');
1334
+ if (ceScope(el.ownerDocument)) { withReactions(setOuterHTML, null, [el, html]); return; }
1335
+ const nodes = parseMarkup(parent.nodeType === ELEMENT_NODE ? parent : null, html, el.ownerDocument);
1336
+ insertBatch(parent, nodes, el.nextSibling, [el]);
1337
+ }
1338
+
1339
+ // ChildNode before()/prepend(): the first preceding sibling of `child` (null: from the
1340
+ // end) that is not one of the nodes being moved.
1341
+ function viablePreviousSibling(parent, child, nodes) {
1342
+ let n = child === null ? null : child.previousSibling;
1343
+ while (n !== null && nodes.includes(n)) n = n.previousSibling;
1344
+ return n;
1345
+ }
1346
+
1347
+ // childList records are batched by insertBatch while this is non-zero.
1348
+ let moQuiet = 0;
1349
+ const observed = (node) => { const d = node.ownerDocument; return d !== null && d.__mo != null && d.__mo.length !== 0; };
1350
+
1351
+ // DOM "insert a fragment" / "replace all" for the multi-node APIs: the nodes leave their old
1352
+ // parents (recorded there), \`replaced\` children of \`parent\` go, and the nodes go in before
1353
+ // \`ref\` (after it when \`afterRef\`; null = at the start) — ONE childList record, as in Chrome.
1354
+ function insertBatch(parent, nodes, ref, replaced, afterRef = false) {
1355
+ for (const n of nodes) assertNotAncestor(parent, n);
1356
+ const flat = [];
1357
+ for (const n of nodes) {
1358
+ if (n.nodeType === DOCUMENT_FRAGMENT_NODE) {
1359
+ const kids = n.__children().slice();
1360
+ moQuiet++;
1361
+ try { for (const k of kids) n.removeChild(k); } finally { moQuiet--; }
1362
+ if (kids.length !== 0) notifyMutation(n, 'childList', EMPTY_NODES, kids, null, null, null, null);
1363
+ for (const k of kids) flat.push(k);
1364
+ } else {
1365
+ if (n.parentNode) n.parentNode.removeChild(n);
1366
+ flat.push(n);
1367
+ }
1368
+ }
1369
+ const before = afterRef ? (ref === null ? parent.firstChild : ref.nextSibling) : ref;
1370
+ const gone = replaced.filter((c) => c.parentNode === parent);
1371
+ let previous;
1372
+ moQuiet++;
1373
+ try {
1374
+ for (const c of gone) parent.removeChild(c);
1375
+ previous = before === null ? parent.lastChild : before.previousSibling;
1376
+ for (const n of flat) parent.insertBefore(n, before);
1377
+ } finally { moQuiet--; }
1378
+ if (flat.length !== 0 || gone.length !== 0) notifyMutation(parent, 'childList', flat, gone, before, null, null, previous);
1379
+ }
1380
+
1381
+ // Deep clones of `source`'s children, created in `doc` and parented to `parent`.
1382
+ function cloneKids(source, parent, doc) {
1383
+ const src = source.__children(), kids = new Array(src.length);
1384
+ for (let i = 0; i < src.length; i++) { const cl = src[i].__clone(doc, true); cl.parentNode = parent; kids[i] = cl; }
1385
+ return kids;
1386
+ }
1387
+
1109
1388
  // First <slot> within a shadow tree whose name matches (default slot: name '').
1110
1389
  function findSlot(shadowRoot, name) {
1111
1390
  let found = null;
@@ -1123,15 +1402,21 @@ function findSlot(shadowRoot, name) {
1123
1402
  return found;
1124
1403
  }
1125
1404
 
1126
- function collectByTag(root, tag) {
1405
+ // The elements with qualified name `raw` (DOM "list of elements with qualified name"): HTML
1406
+ // elements compare against the lowercased `tag`, others (SVG camelCase, other namespaces)
1407
+ // against `raw` exactly. Only a name with a colon can match a prefixed element.
1408
+ function collectByTag(root, raw, tag = raw) {
1127
1409
  const out = [];
1128
- const all = tag === '*';
1410
+ const all = raw === '*';
1411
+ const colon = raw.indexOf(':') !== -1;
1129
1412
  const visit = (node) => {
1130
1413
  const kids = node.__children();
1131
1414
  for (let i = 0; i < kids.length; i++) {
1132
1415
  const c = kids[i];
1133
1416
  if (c.nodeType !== ELEMENT_NODE) continue;
1134
- if (all || c.localName === tag) out.push(c);
1417
+ if (all) out.push(c);
1418
+ else if (c.__ns === '') { if (c.localName === tag ? c.__prefix === undefined : colon && c.__prefix !== undefined && `${c.__prefix}:${c.localName}` === tag) out.push(c); }
1419
+ else if (c.localName === raw ? c.__prefix === undefined : colon && c.__prefix !== undefined && `${c.__prefix}:${c.localName}` === raw) out.push(c);
1135
1420
  visit(c);
1136
1421
  }
1137
1422
  };
@@ -1227,91 +1512,327 @@ function nodeIndex(node) {
1227
1512
  return p.__children().indexOf(node);
1228
1513
  }
1229
1514
 
1230
- // Range — functional DOM Range. Tree-mutating ops (extract/clone/delete/insert/
1231
- // surround) implement the common case (a single container with element/text
1232
- // children, offsets as child indices). Zero geometry (no layout).
1515
+ // Range — DOM Range boundary-point algorithms (DOM §5.2–5.5, miaskiewicz/turbo-dom#207).
1516
+ // Boundary points are (node, offset); offsets are child indices, or code-unit offsets in a
1517
+ // CharacterData node. Not live: a mutation made outside the range's own methods does not
1518
+ // update its boundary points. Zero geometry (no layout).
1519
+ const isCharData = (n) => n.nodeType === TEXT_NODE || n.nodeType === COMMENT_NODE;
1520
+ const nodeLength = (n) => (n.nodeType === DOCUMENT_TYPE_NODE ? 0 : isCharData(n) ? n.length : n.__children().length);
1521
+ function rootOf(n) { while (n.parentNode) n = n.parentNode; return n; }
1522
+ const isInclusiveAncestor = (a, n) => { for (; n; n = n.parentNode) if (n === a) return true; return false; };
1523
+ // tree order: true when `a` precedes `b` (same root assumed).
1524
+ function precedes(a, b) {
1525
+ const chain = (n) => { const c = []; for (; n; n = n.parentNode) c.unshift(n); return c; };
1526
+ const ca = chain(a), cb = chain(b);
1527
+ let i = 0;
1528
+ while (i < ca.length && i < cb.length && ca[i] === cb[i]) i++;
1529
+ if (i === ca.length) return i !== cb.length; // a is an ancestor of b (or b itself)
1530
+ if (i === cb.length) return false; // b is an ancestor of a
1531
+ return nodeIndex(ca[i]) < nodeIndex(cb[i]);
1532
+ }
1533
+ // -1 before, 0 equal, 1 after: the position of (nA, oA) relative to (nB, oB).
1534
+ function compareBoundary(nA, oA, nB, oB) {
1535
+ if (nA === nB) return Math.sign(oA - oB);
1536
+ if (precedes(nB, nA)) return -compareBoundary(nB, oB, nA, oA);
1537
+ if (isInclusiveAncestor(nA, nB)) {
1538
+ let child = nB;
1539
+ while (child.parentNode !== nA) child = child.parentNode;
1540
+ if (nodeIndex(child) < oA) return 1;
1541
+ }
1542
+ return -1;
1543
+ }
1544
+ function checkPoint(node, offset) {
1545
+ if (node.nodeType === DOCUMENT_TYPE_NODE) throw domError('InvalidNodeTypeError', 'The node is a doctype.');
1546
+ if (offset > nodeLength(node)) throw domError('IndexSizeError', `There is no child at offset ${offset}.`);
1547
+ }
1548
+ function parentOrThrow(node) {
1549
+ const p = node.parentNode;
1550
+ if (!p) throw domError('InvalidNodeTypeError', 'The node has no parent.');
1551
+ return p;
1552
+ }
1553
+
1233
1554
  class Range {
1555
+ static START_TO_START = 0; static START_TO_END = 1; static END_TO_END = 2; static END_TO_START = 3;
1556
+ get START_TO_START() { return 0; }
1557
+ get START_TO_END() { return 1; }
1558
+ get END_TO_END() { return 2; }
1559
+ get END_TO_START() { return 3; }
1234
1560
  constructor(doc) {
1235
1561
  this.__doc = doc;
1236
1562
  this.startContainer = doc; this.endContainer = doc;
1237
- this.startOffset = 0; this.endOffset = 0; this.collapsed = true;
1238
- }
1239
- setStart(node, offset) { this.startContainer = node; this.startOffset = offset; this.__sync(); }
1240
- setEnd(node, offset) { this.endContainer = node; this.endOffset = offset; this.__sync(); }
1241
- setStartBefore(node) { this.setStart(node.parentNode, nodeIndex(node)); }
1242
- setStartAfter(node) { this.setStart(node.parentNode, nodeIndex(node) + 1); }
1243
- setEndBefore(node) { this.setEnd(node.parentNode, nodeIndex(node)); }
1244
- setEndAfter(node) { this.setEnd(node.parentNode, nodeIndex(node) + 1); }
1563
+ this.startOffset = 0; this.endOffset = 0;
1564
+ liveRanges.push(new WeakRef(this));
1565
+ }
1566
+ get collapsed() { return this.startContainer === this.endContainer && this.startOffset === this.endOffset; }
1567
+ __set(isStart, node, offset) {
1568
+ offset >>>= 0;
1569
+ checkPoint(node, offset);
1570
+ const [n, o] = isStart ? [this.endContainer, this.endOffset] : [this.startContainer, this.startOffset];
1571
+ const otherRoot = rootOf(n) !== rootOf(node);
1572
+ if (isStart) {
1573
+ if (otherRoot || compareBoundary(node, offset, n, o) > 0) { this.endContainer = node; this.endOffset = offset; }
1574
+ this.startContainer = node; this.startOffset = offset;
1575
+ } else {
1576
+ if (otherRoot || compareBoundary(node, offset, n, o) < 0) { this.startContainer = node; this.startOffset = offset; }
1577
+ this.endContainer = node; this.endOffset = offset;
1578
+ }
1579
+ }
1580
+ setStart(node, offset) { this.__set(true, node, offset); }
1581
+ setEnd(node, offset) { this.__set(false, node, offset); }
1582
+ setStartBefore(node) { this.setStart(parentOrThrow(node), nodeIndex(node)); }
1583
+ setStartAfter(node) { this.setStart(parentOrThrow(node), nodeIndex(node) + 1); }
1584
+ setEndBefore(node) { this.setEnd(parentOrThrow(node), nodeIndex(node)); }
1585
+ setEndAfter(node) { this.setEnd(parentOrThrow(node), nodeIndex(node) + 1); }
1245
1586
  // selectNode: container is the node's parent, offsets bracket the node.
1246
- selectNode(node) { const p = node.parentNode, i = nodeIndex(node); this.startContainer = this.endContainer = p; this.startOffset = i; this.endOffset = i + 1; this.__sync(); }
1247
- selectNodeContents(node) { this.startContainer = this.endContainer = node; this.startOffset = 0; this.endOffset = node.nodeType === TEXT_NODE || node.nodeType === COMMENT_NODE ? node.length : node.__children().length; this.__sync(); }
1248
- collapse(toStart) { if (toStart) { this.endContainer = this.startContainer; this.endOffset = this.startOffset; } else { this.startContainer = this.endContainer; this.startOffset = this.endOffset; } this.collapsed = true; }
1249
- __sync() { this.collapsed = this.startContainer === this.endContainer && this.startOffset === this.endOffset; }
1587
+ selectNode(node) { const p = parentOrThrow(node), i = nodeIndex(node); this.startContainer = this.endContainer = p; this.startOffset = i; this.endOffset = i + 1; }
1588
+ selectNodeContents(node) {
1589
+ if (node.nodeType === DOCUMENT_TYPE_NODE) throw domError('InvalidNodeTypeError', 'The node is a doctype.');
1590
+ this.startContainer = this.endContainer = node; this.startOffset = 0; this.endOffset = nodeLength(node);
1591
+ }
1592
+ collapse(toStart) { if (toStart) { this.endContainer = this.startContainer; this.endOffset = this.startOffset; } else { this.startContainer = this.endContainer; this.startOffset = this.endOffset; } }
1250
1593
  // common ancestor: walk up from start until it contains end (per spec).
1251
1594
  get commonAncestorContainer() {
1252
1595
  let container = this.startContainer;
1253
- while (container && !(container === this.endContainer || (container.contains && container.contains(this.endContainer)))) {
1254
- container = container.parentNode;
1255
- }
1256
- return container || this.startContainer;
1257
- }
1258
- cloneRange() { const r = new Range(this.__doc); r.startContainer = this.startContainer; r.endContainer = this.endContainer; r.startOffset = this.startOffset; r.endOffset = this.endOffset; r.collapsed = this.collapsed; return r; }
1259
- // The common single-container case: start === end container, offsets are child
1260
- // indices (element children) or string offsets (a text/comment container).
1261
- __sameContainerKids() { return this.startContainer === this.endContainer; }
1262
- cloneContents() {
1596
+ while (!isInclusiveAncestor(container, this.endContainer)) container = container.parentNode;
1597
+ return container;
1598
+ }
1599
+ cloneRange() { const r = new Range(this.__doc); r.startContainer = this.startContainer; r.endContainer = this.endContainer; r.startOffset = this.startOffset; r.endOffset = this.endOffset; return r; }
1600
+ compareBoundaryPoints(how, source) {
1601
+ how = Number(how);
1602
+ if (!(how >= 0 && how <= 3)) throw domError('NotSupportedError', 'The comparison method is not supported.');
1603
+ if (rootOf(this.startContainer) !== rootOf(source.startContainer)) throw domError('WrongDocumentError', 'The two ranges are in different trees.');
1604
+ const thisEnd = how === 1 || how === 2, srcEnd = how === 2 || how === 3;
1605
+ return compareBoundary(
1606
+ thisEnd ? this.endContainer : this.startContainer, thisEnd ? this.endOffset : this.startOffset,
1607
+ srcEnd ? source.endContainer : source.startContainer, srcEnd ? source.endOffset : source.startOffset);
1608
+ }
1609
+ comparePoint(node, offset) {
1610
+ if (rootOf(node) !== rootOf(this.startContainer)) throw domError('WrongDocumentError', 'The node is in a different tree.');
1611
+ offset >>>= 0;
1612
+ checkPoint(node, offset);
1613
+ if (compareBoundary(node, offset, this.startContainer, this.startOffset) < 0) return -1;
1614
+ if (compareBoundary(node, offset, this.endContainer, this.endOffset) > 0) return 1;
1615
+ return 0;
1616
+ }
1617
+ isPointInRange(node, offset) {
1618
+ if (rootOf(node) !== rootOf(this.startContainer)) return false;
1619
+ return this.comparePoint(node, offset) === 0;
1620
+ }
1621
+ intersectsNode(node) {
1622
+ if (rootOf(node) !== rootOf(this.startContainer)) return false;
1623
+ const p = node.parentNode;
1624
+ if (!p) return true;
1625
+ const i = nodeIndex(node);
1626
+ return compareBoundary(p, i, this.endContainer, this.endOffset) < 0 && compareBoundary(p, i + 1, this.startContainer, this.startOffset) > 0;
1627
+ }
1628
+ // a node is contained when it lies wholly between the two boundary points.
1629
+ __contains(n) {
1630
+ return rootOf(n) === rootOf(this.startContainer) && compareBoundary(n, 0, this.startContainer, this.startOffset) > 0
1631
+ && compareBoundary(n, nodeLength(n), this.endContainer, this.endOffset) < 0;
1632
+ }
1633
+ // where a range collapses after extract/delete: the start, or just after its
1634
+ // highest ancestor that does not contain the end.
1635
+ __collapsePoint() {
1636
+ const sc = this.startContainer;
1637
+ if (isInclusiveAncestor(sc, this.endContainer)) return [sc, this.startOffset];
1638
+ let ref = sc;
1639
+ while (!isInclusiveAncestor(ref.parentNode, this.endContainer)) ref = ref.parentNode;
1640
+ return [ref.parentNode, nodeIndex(ref) + 1];
1641
+ }
1642
+ // DOM "extract" (move = true) and "clone the contents" (move = false).
1643
+ __extract(move) {
1263
1644
  const frag = this.__doc.createDocumentFragment();
1264
- const c = this.startContainer;
1265
- if (this.__sameContainerKids() && c && c.nodeType !== TEXT_NODE && c.nodeType !== COMMENT_NODE && c.__children) {
1266
- const kids = c.__children();
1267
- for (let i = this.startOffset; i < this.endOffset && i < kids.length; i++) frag.appendChild(kids[i].cloneNode(true));
1645
+ if (this.collapsed) return frag;
1646
+ const sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1647
+ const cutData = (node, from, to) => {
1648
+ const copy = node.cloneNode(false);
1649
+ copy.data = node.substringData(from, to - from);
1650
+ frag.appendChild(copy);
1651
+ if (move) node.replaceData(from, to - from, '');
1652
+ };
1653
+ if (sc === ec && isCharData(sc)) { cutData(sc, so, eo); if (move) this.collapse(true); return frag; }
1654
+ const common = this.commonAncestorContainer;
1655
+ const childOf = (n) => { while (n.parentNode !== common) n = n.parentNode; return n; };
1656
+ const firstPartial = isInclusiveAncestor(sc, ec) ? null : childOf(sc);
1657
+ const lastPartial = isInclusiveAncestor(ec, sc) ? null : childOf(ec);
1658
+ const contained = common.__children().filter((c) => this.__contains(c));
1659
+ if (contained.some((c) => c.nodeType === DOCUMENT_TYPE_NODE)) throw domError('HierarchyRequestError', 'A doctype cannot be extracted.');
1660
+ const [newNode, newOffset] = move ? this.__collapsePoint() : [null, 0];
1661
+ const partial = (node, from, fromOff, to, toOff) => {
1662
+ const copy = node.cloneNode(false);
1663
+ frag.appendChild(copy);
1664
+ const sub = new Range(this.__doc);
1665
+ sub.startContainer = from; sub.startOffset = fromOff; sub.endContainer = to; sub.endOffset = toOff;
1666
+ copy.appendChild(sub.__extract(move));
1667
+ };
1668
+ if (firstPartial) {
1669
+ if (isCharData(firstPartial)) cutData(firstPartial, so, nodeLength(firstPartial));
1670
+ else partial(firstPartial, sc, so, firstPartial, nodeLength(firstPartial));
1671
+ }
1672
+ for (const c of contained) frag.appendChild(move ? c : c.cloneNode(true));
1673
+ if (lastPartial) {
1674
+ if (isCharData(lastPartial)) cutData(lastPartial, 0, eo);
1675
+ else partial(lastPartial, lastPartial, 0, ec, eo);
1268
1676
  }
1677
+ if (move) { this.startContainer = this.endContainer = newNode; this.startOffset = this.endOffset = newOffset; }
1269
1678
  return frag;
1270
1679
  }
1680
+ cloneContents() { return this.__extract(false); }
1271
1681
  extractContents() {
1272
- const frag = this.__doc.createDocumentFragment();
1273
- const c = this.startContainer;
1274
- if (this.__sameContainerKids() && c && c.nodeType !== TEXT_NODE && c.nodeType !== COMMENT_NODE && c.__children) {
1275
- const kids = c.__children().slice(this.startOffset, this.endOffset);
1276
- for (const k of kids) frag.appendChild(k); // moves out of the tree
1277
- }
1278
- this.collapse(true);
1279
- return frag;
1682
+ if (ceScope(this.__doc)) return withReactions(METHOD.extractContents, this, []);
1683
+ return this.__extract(true);
1280
1684
  }
1281
1685
  deleteContents() {
1282
- const c = this.startContainer;
1283
- if (this.__sameContainerKids() && c && c.nodeType !== TEXT_NODE && c.nodeType !== COMMENT_NODE && c.__children) {
1284
- const kids = c.__children().slice(this.startOffset, this.endOffset);
1285
- for (const k of kids) c.removeChild(k);
1686
+ if (ceScope(this.__doc)) { withReactions(METHOD.deleteContents, this, []); return; }
1687
+ if (this.collapsed) return;
1688
+ const sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1689
+ if (sc === ec && isCharData(sc)) { sc.replaceData(so, eo - so, ''); this.collapse(true); return; }
1690
+ // contained nodes whose parent is not contained, in tree order
1691
+ const doomed = [];
1692
+ (function walk(r, n) { for (const c of n.__children()) { if (r.__contains(c)) doomed.push(c); else walk(r, c); } })(this, this.commonAncestorContainer);
1693
+ const [newNode, newOffset] = this.__collapsePoint();
1694
+ if (isCharData(sc)) sc.replaceData(so, sc.length - so, '');
1695
+ for (const n of doomed) n.parentNode.removeChild(n);
1696
+ if (isCharData(ec)) ec.replaceData(0, eo, '');
1697
+ this.startContainer = this.endContainer = newNode; this.startOffset = this.endOffset = newOffset;
1698
+ }
1699
+ // DOM "insert": splits a Text start node; the end point follows the insertion
1700
+ // (the live-range steps for the split and the insert, applied to this range).
1701
+ insertNode(node) {
1702
+ const sc = this.startContainer, so = this.startOffset;
1703
+ if (sc.nodeType === COMMENT_NODE || (sc.nodeType === TEXT_NODE && !sc.parentNode) || sc === node) {
1704
+ throw domError('HierarchyRequestError', 'The node cannot be inserted at the range start.');
1705
+ }
1706
+ const wasCollapsed = this.collapsed;
1707
+ let ref = sc.nodeType === TEXT_NODE ? sc : (sc.__children()[so] ?? null);
1708
+ const parent = ref ? ref.parentNode : sc;
1709
+ if (sc.nodeType === TEXT_NODE) {
1710
+ const at = nodeIndex(sc);
1711
+ ref = sc.splitText(so);
1286
1712
  }
1287
- this.collapse(true);
1713
+ if (node === ref) ref = ref.nextSibling;
1714
+ // the live-range steps (split, removal, insertion) move this range's end like any other's
1715
+ if (node.parentNode) node.parentNode.removeChild(node);
1716
+ const at = ref ? nodeIndex(ref) : nodeLength(parent);
1717
+ const count = node.nodeType === DOCUMENT_FRAGMENT_NODE ? nodeLength(node) : 1;
1718
+ parent.insertBefore(node, ref);
1719
+ if (wasCollapsed) { this.endContainer = parent; this.endOffset = at + count; }
1288
1720
  }
1289
- insertNode(node) { if (this.startContainer && this.startContainer.insertBefore) this.startContainer.insertBefore(node, this.startContainer.__children ? (this.startContainer.__children()[this.startOffset] ?? null) : null); }
1290
1721
  surroundContents(node) {
1291
- const frag = this.extractContents();
1722
+ if (ceScope(this.__doc)) { withReactions(METHOD.surroundContents, this, [node]); return; }
1723
+ // a non-Text node partially contained: an ancestor of one boundary but not the other
1724
+ const partly = (from, other) => { for (let n = from; n; n = n.parentNode) if (!isInclusiveAncestor(n, other) && n.nodeType !== TEXT_NODE) return true; return false; };
1725
+ if (partly(this.startContainer, this.endContainer) || partly(this.endContainer, this.startContainer)) {
1726
+ throw domError('InvalidStateError', 'The range partially selects a non-Text node.');
1727
+ }
1728
+ const t = node.nodeType;
1729
+ if (t === DOCUMENT_NODE || t === DOCUMENT_TYPE_NODE || t === DOCUMENT_FRAGMENT_NODE) throw domError('InvalidNodeTypeError', 'The node cannot surround a range.');
1730
+ const frag = this.__extract(true);
1731
+ while (node.firstChild) node.removeChild(node.firstChild);
1292
1732
  this.insertNode(node);
1293
1733
  node.appendChild(frag);
1734
+ this.selectNode(node);
1294
1735
  }
1295
- // textual content of the range: the common single-container case.
1736
+ // the range's text: the Text data between the boundary points, in tree order.
1296
1737
  toString() {
1297
- const c = this.startContainer;
1298
- if (c && (c.nodeType === TEXT_NODE || c.nodeType === COMMENT_NODE)) {
1299
- if (this.__sameContainerKids()) return c.data.slice(this.startOffset, this.endOffset);
1300
- return c.data.slice(this.startOffset);
1301
- }
1302
- if (this.__sameContainerKids() && c && c.__children) {
1303
- let s = '';
1304
- const kids = c.__children();
1305
- for (let i = this.startOffset; i < this.endOffset && i < kids.length; i++) s += kids[i].textContent;
1306
- return s;
1307
- }
1308
- return '';
1738
+ const sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1739
+ if (sc === ec && sc.nodeType === TEXT_NODE) return sc.data.slice(so, eo);
1740
+ let s = sc.nodeType === TEXT_NODE ? sc.data.slice(so) : '';
1741
+ (function walk(r, n) { for (const c of n.__children()) { if (c.nodeType === TEXT_NODE && r.__contains(c)) s += c.data; walk(r, c); } })(this, this.commonAncestorContainer);
1742
+ if (ec.nodeType === TEXT_NODE) s += ec.data.slice(0, eo);
1743
+ return s;
1744
+ }
1745
+ // DOM Parsing "createContextualFragment": parse in the start node's element context (a
1746
+ // Text/Comment start uses its parent; a document, fragment or <html> context parses as body).
1747
+ createContextualFragment(html) {
1748
+ if (arguments.length === 0) throw new TypeError("Failed to execute 'createContextualFragment' on 'Range': 1 argument required, but only 0 present.");
1749
+ const sc = this.startContainer;
1750
+ let context = sc.nodeType === ELEMENT_NODE ? sc : isCharData(sc) ? sc.parentElement : null;
1751
+ if (context !== null && context.__ns === '' && context.localName === 'html') context = null;
1752
+ const frag = this.__doc.createDocumentFragment();
1753
+ const nodes = parseMarkup(context, html, this.__doc);
1754
+ for (const n of nodes) n.parentNode = frag;
1755
+ frag.__kids = nodes;
1756
+ return frag;
1309
1757
  }
1310
1758
  getBoundingClientRect() { return zeroRect(); }
1311
1759
  getClientRects() { return []; }
1312
1760
  detach() {}
1313
1761
  }
1314
1762
 
1763
+ // ---- live ranges (DOM "live range" steps; Range objects held weakly) ----
1764
+ let liveRanges = [];
1765
+ function eachLive(fn) {
1766
+ let w = 0;
1767
+ for (let i = 0; i < liveRanges.length; i++) {
1768
+ const r = liveRanges[i].deref();
1769
+ if (r === undefined) continue;
1770
+ liveRanges[w++] = liveRanges[i];
1771
+ fn(r);
1772
+ }
1773
+ liveRanges.length = w;
1774
+ }
1775
+ // node "insert": boundaries in `parent` past `index` shift by `count`
1776
+ function liveInsert(parent, index, count) {
1777
+ eachLive((r) => {
1778
+ if (r.startContainer === parent && r.startOffset > index) r.startOffset += count;
1779
+ if (r.endContainer === parent && r.endOffset > index) r.endOffset += count;
1780
+ });
1781
+ }
1782
+ // node "remove": boundaries inside `node` go to (parent, index); later ones in parent shift
1783
+ function liveRemove(parent, node, index) {
1784
+ eachLive((r) => {
1785
+ if (isInclusiveAncestor(node, r.startContainer)) { r.startContainer = parent; r.startOffset = index; }
1786
+ if (isInclusiveAncestor(node, r.endContainer)) { r.endContainer = parent; r.endOffset = index; }
1787
+ if (r.startContainer === parent && r.startOffset > index) r.startOffset--;
1788
+ if (r.endContainer === parent && r.endOffset > index) r.endOffset--;
1789
+ });
1790
+ }
1791
+ // "replace all": every child goes, so boundaries in or under `parent` end at (parent, 0)
1792
+ function liveReplaceAll(parent) {
1793
+ eachLive((r) => {
1794
+ if (isInclusiveAncestor(parent, r.startContainer)) { r.startContainer = parent; r.startOffset = 0; }
1795
+ if (isInclusiveAncestor(parent, r.endContainer)) { r.endContainer = parent; r.endOffset = 0; }
1796
+ });
1797
+ }
1798
+ // "replace data" of `count` units at `offset` with `added` units
1799
+ function liveData(node, offset, count, added) {
1800
+ eachLive((r) => {
1801
+ if (r.startContainer === node) r.startOffset = dataPoint(r.startOffset, offset, count, added);
1802
+ if (r.endContainer === node) r.endOffset = dataPoint(r.endOffset, offset, count, added);
1803
+ });
1804
+ }
1805
+ function dataPoint(at, offset, count, added) {
1806
+ if (at > offset + count) return at + added - count;
1807
+ return at > offset ? offset : at;
1808
+ }
1809
+ // "split a Text node" (after the new node went in at index + 1)
1810
+ function liveSplit(node, offset, next, parent, index) {
1811
+ eachLive((r) => {
1812
+ if (r.startContainer === node && r.startOffset > offset) { r.startContainer = next; r.startOffset -= offset; }
1813
+ if (r.endContainer === node && r.endOffset > offset) { r.endContainer = next; r.endOffset -= offset; }
1814
+ if (r.startContainer === parent && r.startOffset === index + 1) r.startOffset++;
1815
+ if (r.endContainer === parent && r.endOffset === index + 1) r.endOffset++;
1816
+ });
1817
+ }
1818
+ // normalize: the Text siblings after `node` (at `index`) merge into it
1819
+ function liveMerge(parent, node, index, length) {
1820
+ for (let cur = node.nextSibling, i = index + 1; cur !== null && cur.nodeType === TEXT_NODE; cur = cur.nextSibling, i++) {
1821
+ eachLive((r) => {
1822
+ if (r.startContainer === cur) { r.startContainer = node; r.startOffset += length; }
1823
+ if (r.endContainer === cur) { r.endContainer = node; r.endOffset += length; }
1824
+ if (r.startContainer === parent && r.startOffset === i) { r.startContainer = node; r.startOffset = length; }
1825
+ if (r.endContainer === parent && r.endOffset === i) { r.endContainer = node; r.endOffset = length; }
1826
+ });
1827
+ length += cur.data.length;
1828
+ }
1829
+ }
1830
+ function clampData(node, offset) {
1831
+ offset >>>= 0;
1832
+ if (offset > node._data.length) throw domError('IndexSizeError', `The offset ${offset} is larger than the data's length (${node._data.length}).`);
1833
+ return offset;
1834
+ }
1835
+
1315
1836
  function makeSelection(doc) {
1316
1837
  let ranges = [];
1317
1838
  const sel = {
@@ -1390,72 +1911,121 @@ export class XMLSerializer {
1390
1911
 
1391
1912
  // minimal inline-style CSSOM (honest: only inline + explicitly set props)
1392
1913
  function makeStyle(el) {
1393
- // parse → { values: Map<prop,value>, prio: Map<prop,'important'|''> } (handles !important)
1914
+ // The inline declaration block, re-parsed whenever the `style` attribute changed:
1915
+ // [{ name, value, important, pending }] with shorthands expanded into their longhands
1916
+ // (miaskiewicz/turbo-dom#90, css-shorthand.mjs; the V8 binding's lockstep twin). Longhand
1917
+ // values are kept as authored (colors canonicalized), with no further validation.
1918
+ let source = null, entries = [];
1394
1919
  const parse = () => {
1395
- const values = new Map();
1396
- const prio = new Map();
1397
- for (const decl of (el.getAttribute('style') || '').split(';')) {
1398
- const i = decl.indexOf(':');
1399
- if (i === -1) continue;
1400
- const prop = decl.slice(0, i).trim();
1401
- let val = decl.slice(i + 1).trim();
1402
- if (!prop) continue;
1403
- const m = /\s*!\s*important\s*$/i.exec(val);
1404
- if (m) { val = val.slice(0, m.index).trim(); prio.set(prop, 'important'); }
1405
- values.set(prop, val);
1920
+ const text = el.getAttribute('style') || '';
1921
+ if (text !== source) {
1922
+ source = text;
1923
+ entries = [];
1924
+ for (const [name, value, important] of declarations(text)) {
1925
+ const set = declarationEntries(name, value);
1926
+ if (!set) continue;
1927
+ for (const entry of set) {
1928
+ const index = entries.findIndex((e) => e.name === entry.name);
1929
+ if (index >= 0) {
1930
+ // A repeated property moves to the end; an !important one keeps out a later normal one.
1931
+ if (entries[index].important && !important) continue;
1932
+ entries.splice(index, 1);
1933
+ }
1934
+ entries.push({ ...entry, important });
1935
+ }
1936
+ }
1937
+ }
1938
+ return entries;
1939
+ };
1940
+ const write = () => { source = blockText(entries); el.setAttribute('style', source); };
1941
+ const value = (prop) => {
1942
+ const name = propertyName(prop);
1943
+ const found = parse().find((e) => e.name === name);
1944
+ if (found) return found.value;
1945
+ return canonical(name) ? shorthandValue(name, entries) : '';
1946
+ };
1947
+ const priority = (prop) => {
1948
+ const name = propertyName(prop);
1949
+ const list = longhands(name);
1950
+ const all = parse();
1951
+ const important = (n) => all.some((e) => e.name === n && e.important);
1952
+ return (list ? list.every(important) : important(name)) ? 'important' : '';
1953
+ };
1954
+ const setProperty = (prop, v, prio) => {
1955
+ const text = v === null || v === undefined ? '' : String(v);
1956
+ if (!text.trim()) { removeProperty(prop); return; }
1957
+ const p = prio === null || prio === undefined ? '' : String(prio).toLowerCase();
1958
+ if (p && p !== 'important') return;
1959
+ const set = declarationEntries(prop, text);
1960
+ if (!set) return;
1961
+ const all = parse();
1962
+ for (const entry of set) {
1963
+ const index = all.findIndex((e) => e.name === entry.name);
1964
+ const next = { ...entry, important: p === 'important' };
1965
+ if (index >= 0) all[index] = next; else all.push(next);
1406
1966
  }
1407
- return { values, prio };
1967
+ write();
1408
1968
  };
1409
- const write = ({ values, prio }) => {
1410
- el.setAttribute('style', [...values].map(([k, v]) => `${k}: ${v}${prio.get(k) === 'important' ? ' !important' : ''}`).join('; '));
1969
+ // removeProperty: a shorthand removes its longhands and returns '' (Chrome).
1970
+ const removeProperty = (prop) => {
1971
+ const name = propertyName(prop);
1972
+ const all = parse();
1973
+ const list = longhands(name);
1974
+ if (list) {
1975
+ const kept = all.filter((e) => !list.includes(e.name));
1976
+ if (kept.length !== all.length) { entries = kept; write(); }
1977
+ return '';
1978
+ }
1979
+ const index = all.findIndex((e) => e.name === name);
1980
+ if (index < 0) return '';
1981
+ const [removed] = all.splice(index, 1);
1982
+ write();
1983
+ return removed.value;
1411
1984
  };
1412
1985
  return new Proxy({}, {
1413
1986
  get(_t, key) {
1414
- if (key === 'getPropertyValue') return (p) => styleGet(parse().values, p);
1415
- if (key === 'getPropertyPriority') return (p) => parse().prio.get(p) ?? '';
1416
- if (key === 'setProperty') return (p, v, priority) => { const s = parse(); s.values.set(p, String(v)); if (priority) s.prio.set(p, 'important'); else s.prio.delete(p); write(s); };
1417
- if (key === 'removeProperty') return (p) => { const s = parse(); const v = s.values.get(p) ?? ''; s.values.delete(p); s.prio.delete(p); write(s); return v; };
1418
- if (key === 'item') return (i) => [...parse().values.keys()][i] ?? '';
1419
- if (key === 'length') return parse().values.size;
1420
- if (key === 'cssText') return el.getAttribute('style') || '';
1421
- if (key === 'cssFloat') return parse().values.get('float') ?? '';
1422
- if (key === Symbol.iterator) { const keys = [...parse().values.keys()]; return keys[Symbol.iterator].bind(keys); }
1987
+ if (key === 'getPropertyValue') return value;
1988
+ if (key === 'getPropertyPriority') return priority;
1989
+ if (key === 'setProperty') return setProperty;
1990
+ if (key === 'removeProperty') return removeProperty;
1991
+ if (key === 'item') return (i) => parse()[i]?.name ?? '';
1992
+ if (key === 'length') return parse().length;
1993
+ if (key === 'cssText') return blockText(parse());
1994
+ if (key === 'cssFloat') return value('float');
1995
+ if (key === Symbol.iterator) { const keys = parse().map((e) => e.name); return keys[Symbol.iterator].bind(keys); }
1423
1996
  if (typeof key !== 'string') return undefined;
1424
- return styleGet(parse().values, kebab(key));
1997
+ return value(kebab(key));
1425
1998
  },
1426
- set(_t, key, value) {
1427
- if (key === 'cssText') { el.setAttribute('style', String(value)); return true; }
1428
- const s = parse(); s.values.set(kebab(key), String(value)); write(s); return true;
1999
+ set(_t, key, v) {
2000
+ if (key === 'cssText') { el.setAttribute('style', String(v)); return true; }
2001
+ setProperty(key === 'cssFloat' ? 'float' : kebab(key), v, '');
2002
+ return true;
1429
2003
  },
1430
2004
  has(_t, key) { return typeof key === 'string'; },
1431
2005
  });
1432
2006
  }
1433
- const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
1434
-
1435
- // We store inline styles verbatim (no shorthand expansion). For the common case
1436
- // `style.background='red'; style.backgroundColor` we let a longhand READ fall back
1437
- // to its shorthand when the shorthand holds a single token (one value that applies
1438
- // to all sides / the only sub-property). Multi-token shorthands stay honest: the
1439
- // longhand reads '' rather than guessing which token maps where.
1440
- const SHORTHAND_OF = {
1441
- 'background-color': 'background',
1442
- 'margin-top': 'margin', 'margin-right': 'margin', 'margin-bottom': 'margin', 'margin-left': 'margin',
1443
- 'padding-top': 'padding', 'padding-right': 'padding', 'padding-bottom': 'padding', 'padding-left': 'padding',
1444
- };
1445
- function styleGet(values, prop) {
1446
- let v = values.get(prop);
1447
- if (v === undefined) {
1448
- const sh = SHORTHAND_OF[prop];
1449
- if (sh) { const s = values.get(sh); if (s !== undefined && !/\s/.test(s.trim())) v = s; }
1450
- }
1451
- if (v === undefined) return '';
1452
- // Inline CSSOM canonicalizes <color> values the way a browser does on read-back:
1453
- // hex/rgb()/hsl() → rgb()/rgba(), but NAMED keywords stay as authored (includeNamed
1454
- // =false) — `el.style.color='red'` reads back `'red'`, `'#fff'` reads back rgb().
1455
- // This keeps el.style read-back consistent with getComputedStyle so jest-dom's
1456
- // toHaveStyle (which normalizes the expected value via el.style) compares equal.
1457
- if (COLOR_PROPS.has(prop)) { const c = canonicalizeColor(v, false); if (c) return c; }
1458
- return v;
2007
+ const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()).replace(/^webkit-/, '-webkit-');
2008
+ // A property name as CSSOM keys it: custom properties as authored, others lowercase with
2009
+ // longhand aliases (`word-wrap` -> `overflow-wrap`) resolved.
2010
+ function propertyName(name) {
2011
+ const text = String(name);
2012
+ if (text.startsWith('--')) return text;
2013
+ const lower = text.toLowerCase();
2014
+ return longhandAlias(lower) ?? lower;
2015
+ }
2016
+ // The entries a declaration sets: a shorthand's longhands, or the longhand in Chrome's
2017
+ // spelling (turbo-dom#97: css-shorthand.mjs longhandValue; a value its grammar rejects is
2018
+ // dropped, one without a grammar kept — this runtime has no other property grammar); null
2019
+ // for an invalid value.
2020
+ function declarationEntries(prop, text) {
2021
+ const name = propertyName(prop);
2022
+ if (canonical(name)) return expand(name, text);
2023
+ if (name.startsWith('--')) return [{ name, value: String(text).trim(), pending: null }];
2024
+ const spelled = longhandValue(name, text);
2025
+ if (spelled.valid === false) return null;
2026
+ let v = spelled.text;
2027
+ if (COLOR_PROPS.has(name)) { const c = canonicalizeColor(v, false); if (c) v = c; }
2028
+ return [{ name, value: v, pending: null }];
1459
2029
  }
1460
2030
 
1461
2031
  // WHATWG value-sanitization per input type. Returns the accepted value, or null
@@ -1502,14 +2072,43 @@ function makeDataset(el) {
1502
2072
  // own (old) buffer before being re-homed.
1503
2073
  function adoptInto(node, doc) {
1504
2074
  if (node.ownerDocument === doc) return;
2075
+ const old = node.ownerDocument;
2076
+ // a document and its inert template document share one buffer: keep the lazy links
2077
+ if (old.__buf !== null && old.__buf === doc.__buf) { rehome(node, doc); return; }
2078
+ if (node.__idx >= 0) old.__cache[node.__idx] = undefined; // the old document no longer holds it
1505
2079
  if (node.nodeType === ELEMENT_NODE) {
1506
2080
  if (node.__attrs === undefined && node.__attrIdx >= 0) node.__attrs = node.__buildAttrs();
2081
+ if (node.__attrIdx >= 0 && node.__ns === '') freezeParsedIs(node); // the parser's `is` outlives the buffer link
1507
2082
  node.__attrIdx = -1;
1508
2083
  }
1509
2084
  const kids = node.__children ? node.__children() : []; // inflate off the OLD buffer
1510
2085
  node.__idx = -1;
1511
2086
  node.ownerDocument = doc;
1512
2087
  for (const c of kids) adoptInto(c, doc);
2088
+ if (node.__shadow) adoptInto(node.__shadow, doc); // shadow-including descendants
2089
+ }
2090
+ // Same buffer: switch the owner of the inflated nodes; the rest inflate in `doc` later
2091
+ // (a node's children only exist once its __kids is built, so none is left behind).
2092
+ function rehome(node, doc) {
2093
+ node.ownerDocument = doc;
2094
+ const kids = node.__kids;
2095
+ if (kids) for (let i = 0; i < kids.length; i++) rehome(kids[i], doc);
2096
+ if (node.__shadow) rehome(node.__shadow, doc);
2097
+ }
2098
+
2099
+ // DOM "adopt" (after the node left its old parent): re-home the subtree, bump the old
2100
+ // document's version (its caches must not keep reporting the node), and run the custom
2101
+ // element adoption hooks when either document has them armed. A custom element adopted
2102
+ // into a document without a registry arms that document's hooks (`__ceRegistry = null`:
2103
+ // no definitions, but its custom elements still get connected / disconnected callbacks).
2104
+ function adopt(node, doc) {
2105
+ const old = node.ownerDocument;
2106
+ adoptInto(node, doc);
2107
+ old.__version = ++VERSION;
2108
+ if (old.__ceRegistry !== undefined || doc.__ceRegistry !== undefined) {
2109
+ if (doc.__ceRegistry === undefined) doc.__ceRegistry = null;
2110
+ adoptedNode(node, old, doc);
2111
+ }
1513
2112
  }
1514
2113
 
1515
2114
  // --- MutationObserver, wired to the mutation methods above via notifyMutation ---
@@ -1524,16 +2123,21 @@ const EMPTY_NODES = []; // shared read-only addedNodes/removedNodes for empty re
1524
2123
  // the common case (no MutationObservers — most test files) allocates NOTHING: it
1525
2124
  // only bumps __version and returns. The MutationRecord object + its node arrays
1526
2125
  // are built solely when an observer is actually registered.
1527
- function notifyMutation(target, type, added, removed, nextSibling, attributeName, oldValue) {
2126
+ function notifyMutation(target, type, added, removed, nextSibling, attributeName, oldValue, previousSibling = null) {
1528
2127
  const doc = target.ownerDocument;
1529
2128
  if (!doc) return;
1530
2129
  // bump the DOM version on every structural/attribute mutation — invalidates
1531
2130
  // the document's getElementsBy* caches. Unconditional (independent of observers).
1532
- doc.__version = (doc.__version || 0) + 1;
2131
+ doc.__version = ++VERSION;
1533
2132
  const mo = doc.__mo;
1534
- if (!mo || mo.length === 0) return;
1535
- const addedNodes = added ? [added] : EMPTY_NODES;
1536
- const removedNodes = removed ? [removed] : EMPTY_NODES;
2133
+ if (mo && mo.length !== 0) queueRecords(mo, doc, target, type, added, removed, nextSibling, attributeName, oldValue, previousSibling);
2134
+ // attributeChangedCallback: queued after the mutation record, like Chrome
2135
+ if (type === 'attributes' && doc.__ceRegistry !== undefined) attributeChanged(target, attributeName, oldValue);
2136
+ }
2137
+ function queueRecords(mo, doc, target, type, added, removed, nextSibling, attributeName, oldValue, previousSibling) {
2138
+ // one node, or the node list of a batched change (insertBatch, innerHTML, textContent)
2139
+ const addedNodes = Array.isArray(added) ? added : added ? [added] : EMPTY_NODES;
2140
+ const removedNodes = Array.isArray(removed) ? removed : removed ? [removed] : EMPTY_NODES;
1537
2141
  for (const reg of mo) {
1538
2142
  const { obs, target: obsTarget, options } = reg;
1539
2143
  const onTarget = target === obsTarget;
@@ -1546,7 +2150,7 @@ function notifyMutation(target, type, added, removed, nextSibling, attributeName
1546
2150
  const rec = {
1547
2151
  type, target,
1548
2152
  addedNodes, removedNodes,
1549
- previousSibling: null, nextSibling: nextSibling || null,
2153
+ previousSibling: previousSibling || null, nextSibling: nextSibling || null,
1550
2154
  attributeName: attributeName || null, attributeNamespace: null,
1551
2155
  oldValue: (type === 'attributes' && options.attributeOldValue) ||
1552
2156
  (type === 'characterData' && options.characterDataOldValue) ? (oldValue ?? null) : null,
@@ -1588,9 +2192,31 @@ export class Document extends Node {
1588
2192
  this.defaultView = null; // set by environment (window)
1589
2193
  this.__mo = null; // registered MutationObservers (lazy — most files never observe())
1590
2194
  this.__moPending = null; // observers with queued records awaiting microtask
2195
+ this.__ceRegistry = undefined; // the window's custom element registry, once touched (custom-elements.mjs)
2196
+ this.__inert = undefined; // the template contents owner document (lazy), or true on that document
2197
+ }
2198
+ // HTML "appropriate template contents owner document": one inert document per document
2199
+ // (itself for an inert one) — no browsing context and no registry, so template contents
2200
+ // and their clones never upgrade; it shares the buffer, so buffer-backed contents inflate
2201
+ // lazily from it and move to and from the document without being materialized.
2202
+ __templateDocument() {
2203
+ const inert = this.__inert;
2204
+ if (inert === true) return this;
2205
+ if (inert !== undefined) return inert;
2206
+ const doc = new Document();
2207
+ doc.__buf = this.__buf;
2208
+ doc.__inert = true;
2209
+ doc.__version = ++VERSION;
2210
+ return (this.__inert = doc);
1591
2211
  }
1592
2212
  get nodeType() { return DOCUMENT_NODE; }
1593
2213
  get nodeName() { return '#document'; }
2214
+ // a document clone is a new document; its children are cloned into it
2215
+ __clone(_doc, deep) {
2216
+ const copy = new Document();
2217
+ if (deep) copy.__kids = cloneKids(this, copy, copy);
2218
+ return copy;
2219
+ }
1594
2220
 
1595
2221
  // ---- MutationObserver registry ----
1596
2222
  __moRegister(obs, target, options) {
@@ -1631,7 +2257,7 @@ export class Document extends Node {
1631
2257
  if (tag === 'template') {
1632
2258
  for (let c = buf.firstChild(idx); c !== -1; c = buf.nextSib(c)) {
1633
2259
  if (buf.nodeType(c) === DOCUMENT_FRAGMENT_NODE && buf.tagName(c) === 'content') {
1634
- node.content = this.__nodeAt(c);
2260
+ node.__content = this.__templateDocument().__nodeAt(c); // owned by the inert document
1635
2261
  break;
1636
2262
  }
1637
2263
  }
@@ -1658,9 +2284,11 @@ export class Document extends Node {
1658
2284
  case ELEMENT_NODE:
1659
2285
  node = newElement(this, raw.name, raw.namespace || '');
1660
2286
  node.__attrs = raw.attrs.map((a) => ({ name: a.name, value: a.value, prefix: a.prefix || '' }));
2287
+ // the fragment parser reads `is` at creation, like the initial parse (custom-elements.mjs)
2288
+ if (node.__ns === '' && raw.attrs.length !== 0) parsedIsAttribute(node);
1661
2289
  if (raw.name === 'template') {
1662
2290
  const contentRaw = raw.children.find((c) => c.nodeType === DOCUMENT_FRAGMENT_NODE && c.name === 'content');
1663
- if (contentRaw) { node.content = this.__inflateNested(contentRaw); }
2291
+ if (contentRaw) node.__content = this.__templateDocument().__inflateNested(contentRaw);
1664
2292
  }
1665
2293
  break;
1666
2294
  case TEXT_NODE: node = new Text(this, raw.value); break;
@@ -1691,9 +2319,11 @@ export class Document extends Node {
1691
2319
  this.__active = null;
1692
2320
  this.__mo = []; // drop observers
1693
2321
  this.__moPending = null;
1694
- this.__version = (this.__version || 0) + 1; // invalidate getElementsBy* caches
2322
+ this.__version = ++VERSION; // invalidate getElementsBy* caches
1695
2323
  this.__tagCache = null;
1696
2324
  this.__classCache = null;
2325
+ this.__ceRegistry = undefined; // the window's globals (customElements) reset with the document
2326
+ this.__inert = undefined; // a new buffer gets a new template contents owner document
1697
2327
  }
1698
2328
 
1699
2329
  get documentElement() { return this.__children().find((n) => n.nodeType === ELEMENT_NODE && n.localName === 'html') ?? null; }
@@ -1714,12 +2344,40 @@ export class Document extends Node {
1714
2344
  }
1715
2345
 
1716
2346
  // ---- factories (owned nodes, no buffer) ----
1717
- createElement(tag) { return new Element(this, String(tag).toLowerCase(), ''); }
1718
- createElementNS(ns, qualified) {
1719
- const short = ns === SVG_NS ? 'svg' : ns === MATHML_NS ? 'math' : '';
1720
- const local = qualified.includes(':') ? qualified.split(':')[1] : qualified;
1721
- return newElement(this, local, short);
2347
+ createElement(tag, options) {
2348
+ const raw = String(tag);
2349
+ if (!validLocalName(raw)) throw invalidName(raw);
2350
+ const name = raw.toLowerCase();
2351
+ if (this.__ceRegistry === undefined && options === undefined) return new Element(this, name, '');
2352
+ return createCustomElement(this, name, isOption(options), true);
2353
+ }
2354
+ // DOM "create an element NS": validate and extract the qualified name (Chrome 156 keeps
2355
+ // the local name up to a second colon: 'a:b:c' -> prefix 'a', local name 'b'), then an
2356
+ // element in that namespace — HTML (custom elements), SVG, MathML, or any other / null
2357
+ // namespace (a plain, case-preserving Element).
2358
+ createElementNS(namespace, qualifiedName, options) {
2359
+ const ns = namespace === null || namespace === undefined || namespace === '' ? null : String(namespace);
2360
+ const qualified = String(qualifiedName);
2361
+ let prefix = null, local = qualified;
2362
+ const colon = qualified.indexOf(':');
2363
+ if (colon !== -1) {
2364
+ prefix = qualified.slice(0, colon);
2365
+ const end = qualified.indexOf(':', colon + 1);
2366
+ local = end === -1 ? qualified.slice(colon + 1) : qualified.slice(colon + 1, end);
2367
+ if (!validPrefix(prefix)) throw invalidName(qualified);
2368
+ }
2369
+ if (!validLocalName(local)) throw invalidName(qualified);
2370
+ if ((prefix !== null && ns === null) || (prefix === 'xml' && ns !== XML_NS)
2371
+ || ((qualified === 'xmlns' || prefix === 'xmlns') !== (ns === XMLNS_NS))) {
2372
+ throw domError('NamespaceError', `The namespace and qualified name ('${qualified}') do not match.`);
2373
+ }
2374
+ const el = ns === HTML_NS && (this.__ceRegistry !== undefined || options !== undefined)
2375
+ ? createCustomElement(this, local, isOption(options), true)
2376
+ : newElement(this, local, nsShort(ns));
2377
+ if (prefix !== null) el.__prefix = prefix;
2378
+ return el;
1722
2379
  }
2380
+ __newHtmlElement(localName) { return new Element(this, localName, ''); }
1723
2381
  createTextNode(data) { return new Text(this, String(data)); }
1724
2382
  createComment(data) { return new Comment(this, String(data)); }
1725
2383
  createDocumentFragment() { return new DocumentFragment(this); }
@@ -1736,8 +2394,17 @@ export class Document extends Node {
1736
2394
  createRange() { return new Range(this); }
1737
2395
  createAttribute(name) { return { name, value: '', ownerElement: null }; }
1738
2396
  getSelection() { if (!this.__selection) this.__selection = makeSelection(this); return this.__selection; }
1739
- importNode(node, deep) { return node.cloneNode(deep); }
1740
- adoptNode(node) { if (node.parentNode) node.parentNode.removeChild(node); adoptInto(node, this); return node; }
2397
+ // a clone created in THIS document (custom elements upgrade here, not in the source's)
2398
+ importNode(node, deep) {
2399
+ if (ceScope(this)) return withReactions(METHOD.importNode, this, [node, deep]);
2400
+ return node.__clone(this, !!deep);
2401
+ }
2402
+ adoptNode(node) {
2403
+ if ((this.__ceRegistry !== undefined || node.ownerDocument.__ceRegistry !== undefined) && reactionDepth === 0) return withReactions(METHOD.adoptNode, this, [node]);
2404
+ if (node.parentNode) node.parentNode.removeChild(node);
2405
+ if (node.ownerDocument !== this) adopt(node, this);
2406
+ return node;
2407
+ }
1741
2408
 
1742
2409
  // TreeWalker / NodeIterator (whatToShow: 1 elements, 4 text, 0xFFFFFFFF all)
1743
2410
  createTreeWalker(root, whatToShow = 0xffffffff, filter = null) { return new TreeWalker(root, whatToShow, filter); }
@@ -1797,12 +2464,12 @@ export class Document extends Node {
1797
2464
  // query (e.g. RTL getByLabelText calls element.labels per element, each doing
1798
2465
  // document.getElementsByTagName('label')). Without caching that's O(n²) tree
1799
2466
  // walks. The cache invalidates whenever the DOM version bumps (any mutation).
1800
- __byTag(t) {
2467
+ __byTag(raw, t) {
1801
2468
  const v = this.__version || 0;
1802
- const c = this.__tagCache && this.__tagCache.get(t);
2469
+ const c = this.__tagCache && this.__tagCache.get(raw);
1803
2470
  if (c && c.v === v) return c.arr;
1804
- const arr = collectByTag(this, t);
1805
- (this.__tagCache || (this.__tagCache = new Map())).set(t, { v, arr });
2471
+ const arr = collectByTag(this, raw, t);
2472
+ (this.__tagCache || (this.__tagCache = new Map())).set(raw, { v, arr });
1806
2473
  return arr;
1807
2474
  }
1808
2475
  __byClass(key, classes) {
@@ -1817,10 +2484,10 @@ export class Document extends Node {
1817
2484
  // the version-cached __byTag/__byClass on every access, so a memoized Proxy still
1818
2485
  // reflects mutations — only the per-call Proxy allocation is saved.
1819
2486
  getElementsByTagName(tag) {
1820
- const t = tag.toLowerCase();
2487
+ const raw = String(tag);
1821
2488
  const m = this.__tagColl || (this.__tagColl = new Map());
1822
- let coll = m.get(t);
1823
- if (!coll) { const self = this; coll = liveHTMLCollection(() => self.__byTag(t)); m.set(t, coll); }
2489
+ let coll = m.get(raw);
2490
+ if (!coll) { const self = this, t = raw.toLowerCase(); coll = liveHTMLCollection(() => self.__byTag(raw, t)); m.set(raw, coll); }
1824
2491
  return coll;
1825
2492
  }
1826
2493
  getElementsByClassName(cls) {
@@ -1859,7 +2526,6 @@ export class Document extends Node {
1859
2526
  get visibilityState() { return 'visible'; }
1860
2527
  get hidden() { return false; }
1861
2528
  get readyState() { return 'complete'; }
1862
- hasFocus() { return true; }
1863
2529
  get characterSet() { return 'UTF-8'; }
1864
2530
  get compatMode() { return 'CSS1Compat'; }
1865
2531
  get __owner() { return this.defaultView; }
@@ -1867,6 +2533,26 @@ export class Document extends Node {
1867
2533
 
1868
2534
  export { Event, CustomEvent };
1869
2535
 
2536
+ // The reaction scopes' re-entry targets (see `ceScope`).
2537
+ const setterOf = (proto, name) => Object.getOwnPropertyDescriptor(proto, name).set;
2538
+ Object.assign(METHOD, {
2539
+ insertBefore: Node.prototype.insertBefore,
2540
+ removeChild: Node.prototype.removeChild,
2541
+ replaceChild: Node.prototype.replaceChild,
2542
+ replaceChildren: Node.prototype.replaceChildren,
2543
+ cloneNode: Node.prototype.cloneNode,
2544
+ setTextContent: setterOf(Node.prototype, 'textContent'),
2545
+ prepend: Element.prototype.prepend,
2546
+ setInnerHTML: setterOf(Element.prototype, 'innerHTML'),
2547
+ insertAdjacentHTML: Element.prototype.insertAdjacentHTML,
2548
+ setShadowInnerHTML: setterOf(ShadowRoot.prototype, 'innerHTML'),
2549
+ extractContents: Range.prototype.extractContents,
2550
+ deleteContents: Range.prototype.deleteContents,
2551
+ surroundContents: Range.prototype.surroundContents,
2552
+ importNode: Document.prototype.importNode,
2553
+ adoptNode: Document.prototype.adoptNode,
2554
+ });
2555
+
1870
2556
  // Parse an HTML string into a fresh Document over the immutable SoA buffer.
1871
2557
  export function parseDocument(html) {
1872
2558
  const soa = getParser().parseBuffer(String(html));