@miaskiewicz/turbo-dom 0.5.8 → 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.
- package/Cargo.lock +3175 -392
- package/Cargo.toml +53 -3
- package/README.md +176 -224
- package/package.json +5 -3
- package/pkg/package.json +1 -1
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/package.json +1 -1
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/src/core.rs +387 -44
- package/src/lib.rs +43 -10
- package/src/runtime/buffer.mjs +0 -1
- package/src/runtime/cascade.mjs +18 -24
- package/src/runtime/color.mjs +1 -1
- package/src/runtime/css-shorthand-table.mjs +238 -0
- package/src/runtime/css-shorthand.mjs +2794 -0
- package/src/runtime/cssom.mjs +3 -4
- package/src/runtime/custom-elements.mjs +398 -0
- package/src/runtime/dom.mjs +924 -238
- package/src/runtime/html-serialize.mjs +15 -7
- package/src/runtime/index.d.ts +100 -0
- package/src/runtime/index.mjs +5 -0
- package/src/runtime/parser.mjs +2 -4
- package/src/runtime/selectors.mjs +1080 -402
- package/src/runtime/stubs.mjs +1 -19
- package/src/runtime/window.mjs +15 -7
- package/src/spike.rs +26 -6
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
package/src/runtime/dom.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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.
|
|
154
|
-
|
|
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.
|
|
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
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
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) {
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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) {
|
|
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.
|
|
336
|
-
appendData(s) { this.
|
|
337
|
-
insertData(offset, s) { this.
|
|
338
|
-
deleteData(offset, count) {
|
|
339
|
-
replaceData(offset, count, s) {
|
|
340
|
-
before(...nodes) {
|
|
341
|
-
after(...nodes) {
|
|
342
|
-
replaceWith(...nodes) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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) {
|
|
718
|
-
prepend(...nodes) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
this.
|
|
746
|
-
this
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
case '
|
|
765
|
-
case '
|
|
766
|
-
case '
|
|
767
|
-
|
|
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
|
-
|
|
773
|
-
const el = newElement(
|
|
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
|
-
|
|
779
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1067
|
-
this
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 —
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
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;
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
|
1247
|
-
selectNodeContents(node) {
|
|
1248
|
-
|
|
1249
|
-
|
|
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 (
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1283
|
-
if (this.
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1736
|
+
// the range's text: the Text data between the boundary points, in tree order.
|
|
1296
1737
|
toString() {
|
|
1297
|
-
const
|
|
1298
|
-
if (
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
//
|
|
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
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1967
|
+
write();
|
|
1408
1968
|
};
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
|
1415
|
-
if (key === 'getPropertyPriority') return
|
|
1416
|
-
if (key === 'setProperty') return
|
|
1417
|
-
if (key === 'removeProperty') return
|
|
1418
|
-
if (key === 'item') return (i) =>
|
|
1419
|
-
if (key === 'length') return parse().
|
|
1420
|
-
if (key === 'cssText') return
|
|
1421
|
-
if (key === 'cssFloat') return
|
|
1422
|
-
if (key === Symbol.iterator) { const 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
|
|
1997
|
+
return value(kebab(key));
|
|
1425
1998
|
},
|
|
1426
|
-
set(_t, key,
|
|
1427
|
-
if (key === 'cssText') { el.setAttribute('style', String(
|
|
1428
|
-
|
|
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
|
-
//
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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 =
|
|
2131
|
+
doc.__version = ++VERSION;
|
|
1533
2132
|
const mo = doc.__mo;
|
|
1534
|
-
if (
|
|
1535
|
-
|
|
1536
|
-
|
|
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.
|
|
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)
|
|
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 =
|
|
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
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const
|
|
1721
|
-
return
|
|
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
|
-
|
|
1740
|
-
|
|
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(
|
|
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(
|
|
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
|
|
2487
|
+
const raw = String(tag);
|
|
1821
2488
|
const m = this.__tagColl || (this.__tagColl = new Map());
|
|
1822
|
-
let coll = m.get(
|
|
1823
|
-
if (!coll) { const self = this; coll = liveHTMLCollection(() => self.__byTag(t)); m.set(
|
|
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));
|