@miaskiewicz/turbo-dom 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +588 -0
- package/Cargo.toml +34 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/build.rs +4 -0
- package/index.d.ts +62 -0
- package/index.js +318 -0
- package/package.json +121 -0
- package/src/core.rs +492 -0
- package/src/environment/install.mjs +34 -0
- package/src/environment/jest.cjs +40 -0
- package/src/environment/vitest.mjs +31 -0
- package/src/lib.rs +161 -0
- package/src/runtime/buffer.mjs +35 -0
- package/src/runtime/collections.mjs +50 -0
- package/src/runtime/dom.mjs +863 -0
- package/src/runtime/events.mjs +213 -0
- package/src/runtime/html-serialize.mjs +72 -0
- package/src/runtime/index.mjs +46 -0
- package/src/runtime/selectors.mjs +239 -0
- package/src/runtime/stubs.mjs +148 -0
- package/src/runtime/window.mjs +168 -0
- 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
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
// Layer 2 — lazy node inflation + copy-on-write tree + live collections + identity.
|
|
2
|
+
//
|
|
3
|
+
// The native parser hands us an immutable nested "buffer" (plain JS objects).
|
|
4
|
+
// DOM node handles inflate from it lazily: a node's children aren't built until
|
|
5
|
+
// something reads them. First access memoizes the handle (=== identity preserved).
|
|
6
|
+
// Mutation promotes the affected node to fully-owned (COW). Reads are transparent
|
|
7
|
+
// across the boundary — a buffer-backed read and an owned read are indistinguishable.
|
|
8
|
+
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { EventTarget, Event, CustomEvent } from './events.mjs';
|
|
11
|
+
import { liveNodeList, liveHTMLCollection } from './collections.mjs';
|
|
12
|
+
import { matchesSelector, querySelector as qsel, querySelectorAll as qselAll } from './selectors.mjs';
|
|
13
|
+
import { serializeInner, serializeOuter } from './html-serialize.mjs';
|
|
14
|
+
import { Buffer } from './buffer.mjs';
|
|
15
|
+
import { makeCanvasStub } from './stubs.mjs';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const native = require('../../index.js');
|
|
19
|
+
|
|
20
|
+
export const ELEMENT_NODE = 1;
|
|
21
|
+
export const TEXT_NODE = 3;
|
|
22
|
+
export const COMMENT_NODE = 8;
|
|
23
|
+
export const DOCUMENT_NODE = 9;
|
|
24
|
+
export const DOCUMENT_TYPE_NODE = 10;
|
|
25
|
+
export const DOCUMENT_FRAGMENT_NODE = 11;
|
|
26
|
+
|
|
27
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
28
|
+
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
|
|
29
|
+
const HTML_NS = 'http://www.w3.org/1999/xhtml';
|
|
30
|
+
const nsUri = (short) => (short === 'svg' ? SVG_NS : short === 'math' ? MATHML_NS : HTML_NS);
|
|
31
|
+
|
|
32
|
+
// ------------------------------------------------------------------ Node ----
|
|
33
|
+
export class Node extends EventTarget {
|
|
34
|
+
static ELEMENT_NODE = ELEMENT_NODE;
|
|
35
|
+
static TEXT_NODE = TEXT_NODE;
|
|
36
|
+
static COMMENT_NODE = COMMENT_NODE;
|
|
37
|
+
static DOCUMENT_NODE = DOCUMENT_NODE;
|
|
38
|
+
static DOCUMENT_TYPE_NODE = DOCUMENT_TYPE_NODE;
|
|
39
|
+
static DOCUMENT_FRAGMENT_NODE = DOCUMENT_FRAGMENT_NODE;
|
|
40
|
+
|
|
41
|
+
constructor(ownerDocument) {
|
|
42
|
+
super();
|
|
43
|
+
this.ownerDocument = ownerDocument || null;
|
|
44
|
+
this.parentNode = null;
|
|
45
|
+
this.__idx = -1; // backing buffer index, or -1 if owned/created
|
|
46
|
+
this.__kids = null; // owned child array once inflated/promoted
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Lazy inflation: walk the SoA buffer's firstChild/nextSib for this node, build
|
|
50
|
+
// handles on first access, memoize for identity. Mutation then operates on __kids
|
|
51
|
+
// (COW promotion). A buffer-backed read and an owned read are indistinguishable.
|
|
52
|
+
__children() {
|
|
53
|
+
if (this.__kids) return this.__kids;
|
|
54
|
+
const kids = [];
|
|
55
|
+
const doc = this.ownerDocument;
|
|
56
|
+
if (this.__idx >= 0 && doc && doc.__buf) {
|
|
57
|
+
const buf = doc.__buf;
|
|
58
|
+
for (let c = buf.firstChild(this.__idx); c !== -1; c = buf.nextSib(c)) {
|
|
59
|
+
// template content fragment is not a child — it's `.content`
|
|
60
|
+
if (buf.nodeType(c) === DOCUMENT_FRAGMENT_NODE && buf.tagName(c) === 'content') continue;
|
|
61
|
+
const child = doc.__nodeAt(c);
|
|
62
|
+
child.parentNode = this;
|
|
63
|
+
kids.push(child);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.__kids = kids;
|
|
67
|
+
return kids;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get childNodes() {
|
|
71
|
+
const self = this;
|
|
72
|
+
return liveNodeList(() => self.__children());
|
|
73
|
+
}
|
|
74
|
+
get firstChild() { const k = this.__children(); return k[0] ?? null; }
|
|
75
|
+
get lastChild() { const k = this.__children(); return k[k.length - 1] ?? null; }
|
|
76
|
+
hasChildNodes() { return this.__children().length > 0; }
|
|
77
|
+
|
|
78
|
+
get nextSibling() {
|
|
79
|
+
const p = this.parentNode; if (!p) return null;
|
|
80
|
+
const k = p.__children(); const i = k.indexOf(this);
|
|
81
|
+
return i >= 0 ? k[i + 1] ?? null : null;
|
|
82
|
+
}
|
|
83
|
+
get previousSibling() {
|
|
84
|
+
const p = this.parentNode; if (!p) return null;
|
|
85
|
+
const k = p.__children(); const i = k.indexOf(this);
|
|
86
|
+
return i > 0 ? k[i - 1] : null;
|
|
87
|
+
}
|
|
88
|
+
get parentElement() {
|
|
89
|
+
return this.parentNode && this.parentNode.nodeType === ELEMENT_NODE ? this.parentNode : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- mutation (COW promotion happens implicitly: __children() owns the array) ----
|
|
93
|
+
appendChild(node) { return this.insertBefore(node, null); }
|
|
94
|
+
|
|
95
|
+
insertBefore(node, ref) {
|
|
96
|
+
if (node.nodeType === DOCUMENT_FRAGMENT_NODE) {
|
|
97
|
+
for (const c of node.__children().slice()) this.insertBefore(c, ref);
|
|
98
|
+
return node;
|
|
99
|
+
}
|
|
100
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
101
|
+
const kids = this.__children();
|
|
102
|
+
const i = ref ? kids.indexOf(ref) : -1;
|
|
103
|
+
if (ref && i === -1) throw new Error('NotFoundError: ref is not a child');
|
|
104
|
+
if (ref) kids.splice(i, 0, node); else kids.push(node);
|
|
105
|
+
node.parentNode = this;
|
|
106
|
+
node.ownerDocument = this.ownerDocument;
|
|
107
|
+
notifyMutation(this, { type: 'childList', target: this, addedNodes: [node], removedNodes: [], nextSibling: ref || null });
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
removeChild(node) {
|
|
112
|
+
const kids = this.__children();
|
|
113
|
+
const i = kids.indexOf(node);
|
|
114
|
+
if (i === -1) throw new Error('NotFoundError: node is not a child');
|
|
115
|
+
const next = kids[i + 1] || null;
|
|
116
|
+
kids.splice(i, 1);
|
|
117
|
+
node.parentNode = null;
|
|
118
|
+
notifyMutation(this, { type: 'childList', target: this, addedNodes: [], removedNodes: [node], nextSibling: next });
|
|
119
|
+
return node;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
replaceChild(newNode, oldNode) {
|
|
123
|
+
this.insertBefore(newNode, oldNode);
|
|
124
|
+
this.removeChild(oldNode);
|
|
125
|
+
return oldNode;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
remove() { if (this.parentNode) this.parentNode.removeChild(this); }
|
|
129
|
+
|
|
130
|
+
contains(other) {
|
|
131
|
+
let n = other;
|
|
132
|
+
while (n) { if (n === this) return true; n = n.parentNode; }
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get textContent() {
|
|
137
|
+
let s = '';
|
|
138
|
+
for (const c of this.__children()) {
|
|
139
|
+
if (c.nodeType === TEXT_NODE) s += c.data;
|
|
140
|
+
else if (c.nodeType === ELEMENT_NODE || c.nodeType === DOCUMENT_FRAGMENT_NODE) s += c.textContent;
|
|
141
|
+
}
|
|
142
|
+
return s;
|
|
143
|
+
}
|
|
144
|
+
set textContent(value) {
|
|
145
|
+
this.__kids = [];
|
|
146
|
+
if (value !== '') this.appendChild(this.ownerDocument.createTextNode(String(value)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// window/document path for event propagation past the document
|
|
150
|
+
get __owner() { return null; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ----------------------------------------------------- CharacterData ----
|
|
154
|
+
class CharacterData extends Node {
|
|
155
|
+
constructor(ownerDocument, data) { super(ownerDocument); this._data = data ?? ''; }
|
|
156
|
+
get data() { return this._data; }
|
|
157
|
+
set data(v) { const old = this._data; this._data = String(v); notifyMutation(this, { type: 'characterData', target: this, oldValue: old, addedNodes: [], removedNodes: [] }); }
|
|
158
|
+
get nodeValue() { return this._data; }
|
|
159
|
+
set nodeValue(v) { this.data = v; }
|
|
160
|
+
get length() { return this._data.length; }
|
|
161
|
+
get textContent() { return this._data; }
|
|
162
|
+
set textContent(v) { this.data = v; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class Text extends CharacterData {
|
|
166
|
+
get nodeType() { return TEXT_NODE; }
|
|
167
|
+
get nodeName() { return '#text'; }
|
|
168
|
+
cloneNode() { return new Text(this.ownerDocument, this._data); }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class Comment extends CharacterData {
|
|
172
|
+
get nodeType() { return COMMENT_NODE; }
|
|
173
|
+
get nodeName() { return '#comment'; }
|
|
174
|
+
cloneNode() { return new Comment(this.ownerDocument, this._data); }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class DocumentType extends Node {
|
|
178
|
+
constructor(ownerDocument, name, publicId, systemId) {
|
|
179
|
+
super(ownerDocument);
|
|
180
|
+
this.name = name; this.publicId = publicId || ''; this.systemId = systemId || '';
|
|
181
|
+
}
|
|
182
|
+
get nodeType() { return DOCUMENT_TYPE_NODE; }
|
|
183
|
+
get nodeName() { return this.name; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ------------------------------------------------------------ Element ----
|
|
187
|
+
class ClassList {
|
|
188
|
+
constructor(el) { this.__el = el; }
|
|
189
|
+
__list() { return (this.__el.getAttribute('class') || '').split(/\s+/).filter(Boolean); }
|
|
190
|
+
__set(list) { this.__el.setAttribute('class', list.join(' ')); }
|
|
191
|
+
contains(c) { return this.__list().includes(c); }
|
|
192
|
+
add(...cs) { const l = this.__list(); for (const c of cs) if (!l.includes(c)) l.push(c); this.__set(l); }
|
|
193
|
+
remove(...cs) { this.__set(this.__list().filter((c) => !cs.includes(c))); }
|
|
194
|
+
toggle(c, force) {
|
|
195
|
+
const has = this.contains(c);
|
|
196
|
+
if (force === true || (force === undefined && !has)) { this.add(c); return true; }
|
|
197
|
+
this.remove(c); return false;
|
|
198
|
+
}
|
|
199
|
+
replace(a, b) { const l = this.__list(); const i = l.indexOf(a); if (i === -1) return false; l[i] = b; this.__set(l); return true; }
|
|
200
|
+
get length() { return this.__list().length; }
|
|
201
|
+
item(i) { return this.__list()[i] ?? null; }
|
|
202
|
+
get value() { return this.__el.getAttribute('class') || ''; }
|
|
203
|
+
toString() { return this.value; }
|
|
204
|
+
[Symbol.iterator]() { return this.__list()[Symbol.iterator](); }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export class Element extends Node {
|
|
208
|
+
constructor(ownerDocument, localName, namespace = '') {
|
|
209
|
+
super(ownerDocument);
|
|
210
|
+
this.localName = localName;
|
|
211
|
+
this.__ns = namespace; // '', 'svg', 'math'
|
|
212
|
+
this.__attrs = []; // [{name, value, prefix}]
|
|
213
|
+
this.content = null; // <template> content fragment
|
|
214
|
+
this.shadowRoot = null; // open shadow root, if attached
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get nodeType() { return ELEMENT_NODE; }
|
|
218
|
+
get tagName() { return this.__ns ? this.localName : this.localName.toUpperCase(); }
|
|
219
|
+
get nodeName() { return this.tagName; }
|
|
220
|
+
get namespaceURI() { return nsUri(this.__ns); }
|
|
221
|
+
|
|
222
|
+
// ---- attributes ----
|
|
223
|
+
getAttribute(name) { const a = this.__attrs.find((x) => x.name === name); return a ? a.value : null; }
|
|
224
|
+
hasAttribute(name) { return this.__attrs.some((x) => x.name === name); }
|
|
225
|
+
getAttributeNames() { return this.__attrs.map((a) => a.name); }
|
|
226
|
+
setAttribute(name, value) {
|
|
227
|
+
const a = this.__attrs.find((x) => x.name === name);
|
|
228
|
+
const old = a ? a.value : null;
|
|
229
|
+
if (a) a.value = String(value);
|
|
230
|
+
else this.__attrs.push({ name, value: String(value), prefix: '' });
|
|
231
|
+
notifyMutation(this, { type: 'attributes', target: this, attributeName: name, oldValue: old, addedNodes: [], removedNodes: [] });
|
|
232
|
+
}
|
|
233
|
+
removeAttribute(name) {
|
|
234
|
+
const a = this.__attrs.find((x) => x.name === name);
|
|
235
|
+
this.__attrs = this.__attrs.filter((x) => x.name !== name);
|
|
236
|
+
if (a) notifyMutation(this, { type: 'attributes', target: this, attributeName: name, oldValue: a.value, addedNodes: [], removedNodes: [] });
|
|
237
|
+
}
|
|
238
|
+
toggleAttribute(name, force) {
|
|
239
|
+
const has = this.hasAttribute(name);
|
|
240
|
+
if (force === true || (force === undefined && !has)) { this.setAttribute(name, ''); return true; }
|
|
241
|
+
this.removeAttribute(name); return false;
|
|
242
|
+
}
|
|
243
|
+
get attributes() {
|
|
244
|
+
return this.__attrs.map((a) => ({
|
|
245
|
+
name: a.name, localName: a.name, value: a.value, prefix: a.prefix || null,
|
|
246
|
+
namespaceURI: a.prefix === 'xlink' ? 'http://www.w3.org/1999/xlink' : null,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get id() { return this.getAttribute('id') || ''; }
|
|
251
|
+
set id(v) { this.setAttribute('id', v); }
|
|
252
|
+
|
|
253
|
+
// label / form-control association (used by RTL getByLabelText)
|
|
254
|
+
get htmlFor() { return this.getAttribute('for') || ''; }
|
|
255
|
+
set htmlFor(v) { this.setAttribute('for', v); }
|
|
256
|
+
get control() {
|
|
257
|
+
if (this.localName !== 'label') return null;
|
|
258
|
+
const id = this.getAttribute('for');
|
|
259
|
+
if (id) return this.ownerDocument.getElementById(id);
|
|
260
|
+
return this.querySelector('button,input,select,textarea,meter,output,progress') || null;
|
|
261
|
+
}
|
|
262
|
+
get labels() {
|
|
263
|
+
const labelable = /^(button|input|meter|output|progress|select|textarea)$/.test(this.localName) &&
|
|
264
|
+
!(this.localName === 'input' && this.getAttribute('type') === 'hidden');
|
|
265
|
+
if (!labelable) return undefined;
|
|
266
|
+
const out = [];
|
|
267
|
+
if (this.id) {
|
|
268
|
+
for (const l of this.ownerDocument.getElementsByTagName('label')) {
|
|
269
|
+
if (l.getAttribute('for') === this.id) out.push(l);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
let p = this.parentNode;
|
|
273
|
+
while (p) { if (p.localName === 'label' && !out.includes(p)) out.push(p); p = p.parentNode; }
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
get className() { return this.getAttribute('class') || ''; }
|
|
277
|
+
set className(v) { this.setAttribute('class', v); }
|
|
278
|
+
get classList() { return new ClassList(this); }
|
|
279
|
+
|
|
280
|
+
get dataset() {
|
|
281
|
+
if (!this.__dataset) this.__dataset = makeDataset(this);
|
|
282
|
+
return this.__dataset;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- form-control properties (on the prototype so libraries that read
|
|
286
|
+
// element.constructor.prototype descriptors — e.g. user-event — find them) ----
|
|
287
|
+
get value() {
|
|
288
|
+
const t = this.localName;
|
|
289
|
+
if (t === 'select') {
|
|
290
|
+
const list = Array.from(this.getElementsByTagName('option'));
|
|
291
|
+
const s = list.find((o) => o.selected);
|
|
292
|
+
if (s) return s.value;
|
|
293
|
+
return list.length && !this.multiple ? list[0].value : '';
|
|
294
|
+
}
|
|
295
|
+
if (t === 'option') return this.hasAttribute('value') ? this.getAttribute('value') : this.textContent;
|
|
296
|
+
if (t === 'input' || t === 'textarea') return this.__value !== undefined ? this.__value : (this.getAttribute('value') ?? '');
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
set value(x) {
|
|
300
|
+
const t = this.localName;
|
|
301
|
+
if (t === 'select') { for (const o of this.getElementsByTagName('option')) o.selected = (o.value === String(x)); return; }
|
|
302
|
+
if (t === 'option') { this.setAttribute('value', x); return; }
|
|
303
|
+
this.__value = String(x);
|
|
304
|
+
if (this.__selStart != null) { this.__selStart = Math.min(this.__selStart, this.__value.length); this.__selEnd = Math.min(this.__selEnd, this.__value.length); }
|
|
305
|
+
}
|
|
306
|
+
get defaultValue() { return this.getAttribute('value') ?? ''; }
|
|
307
|
+
set defaultValue(v) { this.setAttribute('value', v); }
|
|
308
|
+
|
|
309
|
+
get selectionStart() { return this.__selStart ?? null; }
|
|
310
|
+
set selectionStart(v) { this.__selStart = v; }
|
|
311
|
+
get selectionEnd() { return this.__selEnd ?? null; }
|
|
312
|
+
set selectionEnd(v) { this.__selEnd = v; }
|
|
313
|
+
get selectionDirection() { return this.__selDir ?? 'none'; }
|
|
314
|
+
set selectionDirection(v) { this.__selDir = v; }
|
|
315
|
+
setSelectionRange(s, e, dir = 'none') { this.__selStart = s; this.__selEnd = e; this.__selDir = dir; }
|
|
316
|
+
setRangeText(repl, start = this.__selStart ?? 0, end = this.__selEnd ?? 0) {
|
|
317
|
+
const v = this.value ?? ''; this.value = v.slice(0, start) + repl + v.slice(end);
|
|
318
|
+
}
|
|
319
|
+
select() { this.__selStart = 0; this.__selEnd = (this.value ?? '').length; }
|
|
320
|
+
|
|
321
|
+
get checked() { return this.__checked !== undefined ? this.__checked : this.hasAttribute('checked'); }
|
|
322
|
+
set checked(x) { this.__checked = !!x; }
|
|
323
|
+
get defaultChecked() { return this.hasAttribute('checked'); }
|
|
324
|
+
set defaultChecked(x) { if (x) this.setAttribute('checked', ''); else this.removeAttribute('checked'); }
|
|
325
|
+
|
|
326
|
+
get type() {
|
|
327
|
+
if (this.localName === 'input') return (this.getAttribute('type') || 'text').toLowerCase();
|
|
328
|
+
if (this.localName === 'button') return (this.getAttribute('type') || 'submit').toLowerCase();
|
|
329
|
+
return this.getAttribute('type') || undefined;
|
|
330
|
+
}
|
|
331
|
+
set type(x) { this.setAttribute('type', x); }
|
|
332
|
+
get disabled() { return this.hasAttribute('disabled'); }
|
|
333
|
+
set disabled(x) { if (x) this.setAttribute('disabled', ''); else this.removeAttribute('disabled'); }
|
|
334
|
+
get readOnly() { return this.hasAttribute('readonly'); }
|
|
335
|
+
set readOnly(x) { if (x) this.setAttribute('readonly', ''); else this.removeAttribute('readonly'); }
|
|
336
|
+
get required() { return this.hasAttribute('required'); }
|
|
337
|
+
set required(x) { if (x) this.setAttribute('required', ''); else this.removeAttribute('required'); }
|
|
338
|
+
get name() { return this.getAttribute('name') ?? ''; }
|
|
339
|
+
set name(x) { this.setAttribute('name', x); }
|
|
340
|
+
get placeholder() { return this.getAttribute('placeholder') ?? ''; }
|
|
341
|
+
set placeholder(x) { this.setAttribute('placeholder', x); }
|
|
342
|
+
get href() { return this.getAttribute('href') ?? ''; }
|
|
343
|
+
set href(x) { this.setAttribute('href', x); }
|
|
344
|
+
|
|
345
|
+
// option
|
|
346
|
+
get selected() { return this.__selected !== undefined ? this.__selected : this.hasAttribute('selected'); }
|
|
347
|
+
set selected(x) { this.__selected = !!x; }
|
|
348
|
+
get defaultSelected() { return this.hasAttribute('selected'); }
|
|
349
|
+
get text() { return this.textContent; }
|
|
350
|
+
set text(v) { this.textContent = v; }
|
|
351
|
+
|
|
352
|
+
// select
|
|
353
|
+
get options() { return this.localName === 'select' ? this.getElementsByTagName('option') : undefined; }
|
|
354
|
+
get multiple() { return this.hasAttribute('multiple'); }
|
|
355
|
+
set multiple(x) { if (x) this.setAttribute('multiple', ''); else this.removeAttribute('multiple'); }
|
|
356
|
+
get selectedOptions() { return Array.from(this.getElementsByTagName('option')).filter((o) => o.selected); }
|
|
357
|
+
get selectedIndex() {
|
|
358
|
+
const list = Array.from(this.getElementsByTagName('option'));
|
|
359
|
+
const i = list.findIndex((o) => o.selected);
|
|
360
|
+
if (i >= 0) return i;
|
|
361
|
+
return list.length && !this.multiple ? 0 : -1;
|
|
362
|
+
}
|
|
363
|
+
set selectedIndex(idx) { Array.from(this.getElementsByTagName('option')).forEach((o, i) => { o.selected = (i === Number(idx)); }); }
|
|
364
|
+
|
|
365
|
+
get style() {
|
|
366
|
+
// minimal honest CSSOM: parse/serialize the inline style attribute
|
|
367
|
+
if (!this.__style) this.__style = makeStyle(this);
|
|
368
|
+
return this.__style;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---- element-only traversal (live) ----
|
|
372
|
+
get children() {
|
|
373
|
+
const self = this;
|
|
374
|
+
return liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE));
|
|
375
|
+
}
|
|
376
|
+
get childElementCount() { return this.__children().filter((n) => n.nodeType === ELEMENT_NODE).length; }
|
|
377
|
+
get firstElementChild() { return this.__children().find((n) => n.nodeType === ELEMENT_NODE) ?? null; }
|
|
378
|
+
get lastElementChild() { const e = this.__children().filter((n) => n.nodeType === ELEMENT_NODE); return e[e.length - 1] ?? null; }
|
|
379
|
+
get nextElementSibling() { let n = this.nextSibling; while (n && n.nodeType !== ELEMENT_NODE) n = n.nextSibling; return n || null; }
|
|
380
|
+
get previousElementSibling() { let n = this.previousSibling; while (n && n.nodeType !== ELEMENT_NODE) n = n.previousSibling; return n || null; }
|
|
381
|
+
|
|
382
|
+
// ---- modern insertion ----
|
|
383
|
+
append(...nodes) { for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
|
|
384
|
+
prepend(...nodes) { const first = this.firstChild; for (const n of nodes) this.insertBefore(toNode(this.ownerDocument, n), first); }
|
|
385
|
+
before(...nodes) { const p = this.parentNode; if (!p) return; for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), this); }
|
|
386
|
+
after(...nodes) { const p = this.parentNode; if (!p) return; const ref = this.nextSibling; for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), ref); }
|
|
387
|
+
replaceWith(...nodes) { const p = this.parentNode; if (!p) return; const ref = this.nextSibling; this.remove(); for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), ref); }
|
|
388
|
+
replaceChildren(...nodes) { this.__kids = []; for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
|
|
389
|
+
|
|
390
|
+
// ---- queries ----
|
|
391
|
+
matches(sel) { return matchesSelector(this, sel); }
|
|
392
|
+
closest(sel) { let n = this; while (n && n.nodeType === ELEMENT_NODE) { if (n.matches(sel)) return n; n = n.parentNode; } return null; }
|
|
393
|
+
querySelector(sel) { return qsel(this, sel); }
|
|
394
|
+
querySelectorAll(sel) { return qselAll(this, sel); }
|
|
395
|
+
getElementsByTagName(tag) { const self = this; return liveHTMLCollection(() => collectByTag(self, tag.toLowerCase())); }
|
|
396
|
+
getElementsByClassName(cls) { const self = this; const classes = cls.split(/\s+/).filter(Boolean); return liveHTMLCollection(() => collectByClass(self, classes)); }
|
|
397
|
+
|
|
398
|
+
// ---- innerHTML / outerHTML ----
|
|
399
|
+
get innerHTML() { return serializeInner(this); }
|
|
400
|
+
set innerHTML(html) {
|
|
401
|
+
const frag = native.parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
|
|
402
|
+
this.__kids = [];
|
|
403
|
+
for (const rawChild of frag.children) {
|
|
404
|
+
if (rawChild.nodeType === DOCUMENT_FRAGMENT_NODE && rawChild.name === 'content') continue;
|
|
405
|
+
const child = this.ownerDocument.__inflateNested(rawChild);
|
|
406
|
+
child.parentNode = this;
|
|
407
|
+
this.__kids.push(child);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
get outerHTML() { return serializeOuter(this); }
|
|
411
|
+
get innerText() { return this.textContent; }
|
|
412
|
+
set innerText(v) { this.textContent = v; }
|
|
413
|
+
|
|
414
|
+
insertAdjacentHTML(position, html) {
|
|
415
|
+
const tmp = this.ownerDocument.createElement(this.localName);
|
|
416
|
+
tmp.innerHTML = html;
|
|
417
|
+
const nodes = tmp.__children().slice();
|
|
418
|
+
const p = this.parentNode;
|
|
419
|
+
switch (position) {
|
|
420
|
+
case 'beforebegin': for (const n of nodes) p.insertBefore(n, this); break;
|
|
421
|
+
case 'afterbegin': { const first = this.firstChild; for (const n of nodes) this.insertBefore(n, first); break; }
|
|
422
|
+
case 'beforeend': for (const n of nodes) this.appendChild(n); break;
|
|
423
|
+
case 'afterend': { const ref = this.nextSibling; for (const n of nodes) p.insertBefore(n, ref); break; }
|
|
424
|
+
default: throw new Error(`bad insertAdjacentHTML position: ${position}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
cloneNode(deep = false) {
|
|
429
|
+
const el = new Element(this.ownerDocument, this.localName, this.__ns);
|
|
430
|
+
el.__attrs = this.__attrs.map((a) => ({ ...a }));
|
|
431
|
+
if (deep) for (const c of this.__children()) el.appendChild(c.cloneNode(true));
|
|
432
|
+
return el;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
click() { this.dispatchEvent(new Event('click', { bubbles: true, cancelable: true })); }
|
|
436
|
+
// default actions applied post-dispatch when not preventDefault'd
|
|
437
|
+
__runDefaultAction(e) {
|
|
438
|
+
if (e.type !== 'click') return;
|
|
439
|
+
if (this.localName === 'input') {
|
|
440
|
+
const t = (this.getAttribute('type') || 'text').toLowerCase();
|
|
441
|
+
if (t === 'checkbox') this.checked = !this.checked;
|
|
442
|
+
else if (t === 'radio') this.checked = true;
|
|
443
|
+
else if (t === 'submit') { const f = this.closest('form'); if (f) f.requestSubmit(); }
|
|
444
|
+
} else if (this.localName === 'button') {
|
|
445
|
+
const t = (this.getAttribute('type') || 'submit').toLowerCase();
|
|
446
|
+
if (t === 'submit') { const f = this.closest('form'); if (f) f.requestSubmit(); }
|
|
447
|
+
} else if (this.localName === 'label') {
|
|
448
|
+
const c = this.control;
|
|
449
|
+
if (c && c !== e.target) c.click();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
focus() {
|
|
453
|
+
this.ownerDocument.__setActive(this);
|
|
454
|
+
this.dispatchEvent(new Event('focus'));
|
|
455
|
+
this.dispatchEvent(new Event('focusin', { bubbles: true }));
|
|
456
|
+
}
|
|
457
|
+
blur() {
|
|
458
|
+
this.ownerDocument.__setActive(this.ownerDocument.body);
|
|
459
|
+
this.dispatchEvent(new Event('blur'));
|
|
460
|
+
this.dispatchEvent(new Event('focusout', { bubbles: true }));
|
|
461
|
+
}
|
|
462
|
+
getBoundingClientRect() { return zeroRect(); }
|
|
463
|
+
getClientRects() { return []; }
|
|
464
|
+
scrollIntoView() {}
|
|
465
|
+
|
|
466
|
+
// canvas (no raster backend — honest no-op context)
|
|
467
|
+
getContext(type) { return this.localName === 'canvas' ? (this.__ctx ||= makeCanvasStub()) : null; }
|
|
468
|
+
toDataURL() { return 'data:,'; }
|
|
469
|
+
|
|
470
|
+
// shadow DOM (open by default; a detached fragment with a host back-reference)
|
|
471
|
+
attachShadow(init = {}) {
|
|
472
|
+
const root = new DocumentFragment(this.ownerDocument);
|
|
473
|
+
root.host = this;
|
|
474
|
+
root.mode = init.mode || 'open';
|
|
475
|
+
root.querySelector = (s) => qsel(root, s);
|
|
476
|
+
root.querySelectorAll = (s) => qselAll(root, s);
|
|
477
|
+
this.__shadow = root;
|
|
478
|
+
if (root.mode === 'open') this.shadowRoot = root;
|
|
479
|
+
return root;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// forms
|
|
483
|
+
get form() { return this.closest ? this.closest('form') : null; }
|
|
484
|
+
get elements() {
|
|
485
|
+
if (this.localName !== 'form') return undefined;
|
|
486
|
+
return liveHTMLCollection(() => collectByTag(this, '*').filter((e) => /^(input|select|textarea|button|fieldset|output)$/.test(e.localName)));
|
|
487
|
+
}
|
|
488
|
+
submit() { if (this.localName === 'form') this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); }
|
|
489
|
+
requestSubmit() { this.submit(); }
|
|
490
|
+
reset() {
|
|
491
|
+
if (this.localName !== 'form') return;
|
|
492
|
+
this.dispatchEvent(new Event('reset', { bubbles: true, cancelable: true }));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------- DocumentFragment ----
|
|
498
|
+
export class DocumentFragment extends Node {
|
|
499
|
+
get nodeType() { return DOCUMENT_FRAGMENT_NODE; }
|
|
500
|
+
get nodeName() { return '#document-fragment'; }
|
|
501
|
+
querySelector(sel) { return qsel(this, sel); }
|
|
502
|
+
querySelectorAll(sel) { return qselAll(this, sel); }
|
|
503
|
+
get children() { const self = this; return liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE)); }
|
|
504
|
+
append(...nodes) { for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
|
|
505
|
+
cloneNode(deep = false) { const f = new DocumentFragment(this.ownerDocument); if (deep) for (const c of this.__children()) f.appendChild(c.cloneNode(true)); return f; }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ------------------------------------------------------------ helpers ----
|
|
509
|
+
function toNode(doc, n) { return typeof n === 'string' ? doc.createTextNode(n) : n; }
|
|
510
|
+
|
|
511
|
+
function collectByTag(root, tag) {
|
|
512
|
+
const out = [];
|
|
513
|
+
const visit = (node) => {
|
|
514
|
+
for (const c of node.__children()) {
|
|
515
|
+
if (c.nodeType === ELEMENT_NODE) {
|
|
516
|
+
if (tag === '*' || c.localName === tag) out.push(c);
|
|
517
|
+
visit(c);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
visit(root);
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
function collectByClass(root, classes) {
|
|
525
|
+
const out = [];
|
|
526
|
+
const visit = (node) => {
|
|
527
|
+
for (const c of node.__children()) {
|
|
528
|
+
if (c.nodeType === ELEMENT_NODE) {
|
|
529
|
+
if (classes.every((cl) => c.classList.contains(cl))) out.push(c);
|
|
530
|
+
visit(c);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
visit(root);
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function zeroRect() {
|
|
539
|
+
return { x: 0, y: 0, top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, toJSON() { return this; } };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Range — functional enough for selection bookkeeping (user-event), zero geometry.
|
|
543
|
+
class Range {
|
|
544
|
+
constructor(doc) {
|
|
545
|
+
this.__doc = doc;
|
|
546
|
+
this.startContainer = doc; this.endContainer = doc;
|
|
547
|
+
this.startOffset = 0; this.endOffset = 0; this.collapsed = true;
|
|
548
|
+
}
|
|
549
|
+
setStart(node, offset) { this.startContainer = node; this.startOffset = offset; this.__sync(); }
|
|
550
|
+
setEnd(node, offset) { this.endContainer = node; this.endOffset = offset; this.__sync(); }
|
|
551
|
+
setStartBefore(node) { this.setStart(node.parentNode, 0); }
|
|
552
|
+
setStartAfter(node) { this.setStart(node.parentNode, 0); }
|
|
553
|
+
setEndBefore(node) { this.setEnd(node.parentNode, 0); }
|
|
554
|
+
setEndAfter(node) { this.setEnd(node.parentNode, 0); }
|
|
555
|
+
selectNode(node) { this.startContainer = this.endContainer = node; this.__sync(); }
|
|
556
|
+
selectNodeContents(node) { this.startContainer = this.endContainer = node; this.startOffset = 0; this.endOffset = node.childNodes ? node.childNodes.length : 0; this.__sync(); }
|
|
557
|
+
collapse(toStart) { if (toStart) { this.endContainer = this.startContainer; this.endOffset = this.startOffset; } else { this.startContainer = this.endContainer; this.startOffset = this.endOffset; } this.collapsed = true; }
|
|
558
|
+
__sync() { this.collapsed = this.startContainer === this.endContainer && this.startOffset === this.endOffset; }
|
|
559
|
+
get commonAncestorContainer() { return this.startContainer; }
|
|
560
|
+
cloneRange() { const r = new Range(this.__doc); Object.assign(r, this); return r; }
|
|
561
|
+
cloneContents() { return this.__doc.createDocumentFragment(); }
|
|
562
|
+
deleteContents() {}
|
|
563
|
+
insertNode(node) { if (this.startContainer && this.startContainer.insertBefore) this.startContainer.insertBefore(node, this.startContainer.childNodes[this.startOffset] ?? null); }
|
|
564
|
+
surroundContents(node) { this.insertNode(node); }
|
|
565
|
+
getBoundingClientRect() { return zeroRect(); }
|
|
566
|
+
getClientRects() { return []; }
|
|
567
|
+
detach() {}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function makeSelection() {
|
|
571
|
+
let ranges = [];
|
|
572
|
+
return {
|
|
573
|
+
get rangeCount() { return ranges.length; },
|
|
574
|
+
get isCollapsed() { return ranges.every((r) => r.collapsed); },
|
|
575
|
+
get anchorNode() { return ranges[0] ? ranges[0].startContainer : null; },
|
|
576
|
+
get focusNode() { return ranges[0] ? ranges[0].endContainer : null; },
|
|
577
|
+
get anchorOffset() { return ranges[0] ? ranges[0].startOffset : 0; },
|
|
578
|
+
get focusOffset() { return ranges[0] ? ranges[0].endOffset : 0; },
|
|
579
|
+
get type() { return ranges.length ? 'Range' : 'None'; },
|
|
580
|
+
addRange(r) { ranges.push(r); },
|
|
581
|
+
removeAllRanges() { ranges = []; },
|
|
582
|
+
removeRange(r) { ranges = ranges.filter((x) => x !== r); },
|
|
583
|
+
getRangeAt(i) { return ranges[i]; },
|
|
584
|
+
collapse() {}, extend() {}, selectAllChildren() {}, setBaseAndExtent() {}, empty() { ranges = []; },
|
|
585
|
+
toString() { return ''; },
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// minimal inline-style CSSOM (honest: only inline + explicitly set props)
|
|
590
|
+
function makeStyle(el) {
|
|
591
|
+
const parse = () => {
|
|
592
|
+
const map = new Map();
|
|
593
|
+
for (const decl of (el.getAttribute('style') || '').split(';')) {
|
|
594
|
+
const i = decl.indexOf(':');
|
|
595
|
+
if (i === -1) continue;
|
|
596
|
+
const prop = decl.slice(0, i).trim();
|
|
597
|
+
const val = decl.slice(i + 1).trim();
|
|
598
|
+
if (prop) map.set(prop, val);
|
|
599
|
+
}
|
|
600
|
+
return map;
|
|
601
|
+
};
|
|
602
|
+
const write = (map) => el.setAttribute('style', [...map].map(([k, v]) => `${k}: ${v}`).join('; '));
|
|
603
|
+
return new Proxy({}, {
|
|
604
|
+
get(_t, key) {
|
|
605
|
+
if (key === 'getPropertyValue') return (p) => parse().get(p) ?? '';
|
|
606
|
+
if (key === 'setProperty') return (p, v) => { const m = parse(); m.set(p, v); write(m); };
|
|
607
|
+
if (key === 'removeProperty') return (p) => { const m = parse(); const v = m.get(p) ?? ''; m.delete(p); write(m); return v; };
|
|
608
|
+
if (key === 'cssText') return el.getAttribute('style') || '';
|
|
609
|
+
if (typeof key !== 'string') return undefined;
|
|
610
|
+
return parse().get(kebab(key)) ?? '';
|
|
611
|
+
},
|
|
612
|
+
set(_t, key, value) {
|
|
613
|
+
if (key === 'cssText') { el.setAttribute('style', String(value)); return true; }
|
|
614
|
+
const m = parse(); m.set(kebab(key), String(value)); write(m); return true;
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
619
|
+
|
|
620
|
+
// element.dataset — camelCase <-> data-* attribute mapping.
|
|
621
|
+
const dataAttr = (key) => 'data-' + key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
622
|
+
const dataKey = (attr) => attr.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
623
|
+
function makeDataset(el) {
|
|
624
|
+
return new Proxy({}, {
|
|
625
|
+
get(_t, k) { if (typeof k !== 'string') return undefined; const v = el.getAttribute(dataAttr(k)); return v === null ? undefined : v; },
|
|
626
|
+
set(_t, k, v) { el.setAttribute(dataAttr(k), String(v)); return true; },
|
|
627
|
+
deleteProperty(_t, k) { el.removeAttribute(dataAttr(k)); return true; },
|
|
628
|
+
has(_t, k) { return el.hasAttribute(dataAttr(k)); },
|
|
629
|
+
ownKeys() { return el.getAttributeNames().filter((n) => n.startsWith('data-')).map(dataKey); },
|
|
630
|
+
getOwnPropertyDescriptor(_t, k) {
|
|
631
|
+
const n = dataAttr(k);
|
|
632
|
+
if (el.hasAttribute(n)) return { configurable: true, enumerable: true, value: el.getAttribute(n) };
|
|
633
|
+
return undefined;
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// --- MutationObserver, wired to the mutation methods above via notifyMutation ---
|
|
639
|
+
function isDescendant(node, ancestor) {
|
|
640
|
+
let n = node;
|
|
641
|
+
while (n) { if (n === ancestor) return true; n = n.parentNode; }
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
function notifyMutation(target, record) {
|
|
645
|
+
const doc = target.ownerDocument;
|
|
646
|
+
if (!doc || !doc.__mo || doc.__mo.length === 0) return;
|
|
647
|
+
for (const reg of doc.__mo) {
|
|
648
|
+
const { obs, target: obsTarget, options } = reg;
|
|
649
|
+
const onTarget = record.target === obsTarget;
|
|
650
|
+
const inSubtree = options.subtree && isDescendant(record.target, obsTarget);
|
|
651
|
+
if (!onTarget && !inSubtree) continue;
|
|
652
|
+
if (record.type === 'childList' && !options.childList) continue;
|
|
653
|
+
if (record.type === 'attributes' && !options.attributes) continue;
|
|
654
|
+
if (record.type === 'attributes' && options.attributeFilter && !options.attributeFilter.includes(record.attributeName)) continue;
|
|
655
|
+
if (record.type === 'characterData' && !options.characterData) continue;
|
|
656
|
+
const rec = {
|
|
657
|
+
type: record.type, target: record.target,
|
|
658
|
+
addedNodes: record.addedNodes || [], removedNodes: record.removedNodes || [],
|
|
659
|
+
previousSibling: record.previousSibling || null, nextSibling: record.nextSibling || null,
|
|
660
|
+
attributeName: record.attributeName || null, attributeNamespace: null,
|
|
661
|
+
oldValue: (record.type === 'attributes' && options.attributeOldValue) ||
|
|
662
|
+
(record.type === 'characterData' && options.characterDataOldValue) ? (record.oldValue ?? null) : null,
|
|
663
|
+
};
|
|
664
|
+
obs.__enqueue(rec);
|
|
665
|
+
doc.__scheduleMO(obs);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export class MutationObserver {
|
|
670
|
+
constructor(callback) { this.__cb = callback; this.__records = []; this.__regs = []; }
|
|
671
|
+
observe(target, options = {}) {
|
|
672
|
+
const opts = {
|
|
673
|
+
childList: !!options.childList,
|
|
674
|
+
attributes: options.attributes ?? (options.attributeFilter || options.attributeOldValue ? true : false),
|
|
675
|
+
characterData: options.characterData ?? (options.characterDataOldValue ? true : false),
|
|
676
|
+
subtree: !!options.subtree,
|
|
677
|
+
attributeOldValue: !!options.attributeOldValue,
|
|
678
|
+
characterDataOldValue: !!options.characterDataOldValue,
|
|
679
|
+
attributeFilter: options.attributeFilter || null,
|
|
680
|
+
};
|
|
681
|
+
const doc = target.ownerDocument || target;
|
|
682
|
+
doc.__moRegister(this, target, opts);
|
|
683
|
+
this.__regs.push(doc);
|
|
684
|
+
}
|
|
685
|
+
disconnect() { for (const doc of this.__regs) doc.__moUnregister(this); this.__regs = []; this.__records = []; }
|
|
686
|
+
takeRecords() { const r = this.__records; this.__records = []; return r; }
|
|
687
|
+
__enqueue(rec) { this.__records.push(rec); }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ----------------------------------------------------------- Document ----
|
|
691
|
+
export class Document extends Node {
|
|
692
|
+
constructor() {
|
|
693
|
+
super(null);
|
|
694
|
+
this.ownerDocument = this;
|
|
695
|
+
this.__buf = null; // SoA buffer accessor
|
|
696
|
+
this.__cache = []; // idx -> handle (identity memoization / nodeAt)
|
|
697
|
+
this.__active = null; // activeElement
|
|
698
|
+
this.defaultView = null; // set by environment (window)
|
|
699
|
+
this.__mo = []; // registered MutationObservers
|
|
700
|
+
this.__moPending = null; // observers with queued records awaiting microtask
|
|
701
|
+
}
|
|
702
|
+
get nodeType() { return DOCUMENT_NODE; }
|
|
703
|
+
get nodeName() { return '#document'; }
|
|
704
|
+
|
|
705
|
+
// ---- MutationObserver registry ----
|
|
706
|
+
__moRegister(obs, target, options) {
|
|
707
|
+
// replace existing registration for (obs,target) per spec
|
|
708
|
+
this.__mo = this.__mo.filter((r) => !(r.obs === obs && r.target === target));
|
|
709
|
+
this.__mo.push({ obs, target, options });
|
|
710
|
+
}
|
|
711
|
+
__moUnregister(obs) { this.__mo = this.__mo.filter((r) => r.obs !== obs); }
|
|
712
|
+
__scheduleMO(obs) {
|
|
713
|
+
if (!this.__moPending) this.__moPending = new Set();
|
|
714
|
+
if (this.__moPending.has(obs)) return;
|
|
715
|
+
this.__moPending.add(obs);
|
|
716
|
+
queueMicrotask(() => {
|
|
717
|
+
this.__moPending.delete(obs);
|
|
718
|
+
const recs = obs.takeRecords();
|
|
719
|
+
if (recs.length) { try { obs.__cb(recs, obs); } catch (e) { /* observer callbacks must not break the mutator */ } }
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// nodeAt: one handle per buffer index, memoized → preserves === identity.
|
|
724
|
+
__nodeAt(idx) {
|
|
725
|
+
if (idx < 0) return null;
|
|
726
|
+
const cached = this.__cache[idx];
|
|
727
|
+
if (cached !== undefined) return cached;
|
|
728
|
+
const buf = this.__buf;
|
|
729
|
+
let node;
|
|
730
|
+
switch (buf.nodeType(idx)) {
|
|
731
|
+
case ELEMENT_NODE: {
|
|
732
|
+
node = new Element(this, buf.tagName(idx), buf.ns(idx));
|
|
733
|
+
node.__idx = idx;
|
|
734
|
+
node.__attrs = buf.attrs(idx);
|
|
735
|
+
// template content fragment: a child node typed 11 named "content"
|
|
736
|
+
if (buf.tagName(idx) === 'template') {
|
|
737
|
+
for (let c = buf.firstChild(idx); c !== -1; c = buf.nextSib(c)) {
|
|
738
|
+
if (buf.nodeType(c) === DOCUMENT_FRAGMENT_NODE && buf.tagName(c) === 'content') {
|
|
739
|
+
node.content = this.__nodeAt(c);
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
case TEXT_NODE: node = new Text(this, buf.text(idx)); node.__idx = idx; break;
|
|
747
|
+
case COMMENT_NODE: node = new Comment(this, buf.text(idx)); node.__idx = idx; break;
|
|
748
|
+
case DOCUMENT_TYPE_NODE:
|
|
749
|
+
node = new DocumentType(this, buf.text(idx), buf.publicId(idx), buf.systemId(idx));
|
|
750
|
+
node.__idx = idx;
|
|
751
|
+
break;
|
|
752
|
+
case DOCUMENT_FRAGMENT_NODE: node = new DocumentFragment(this); node.__idx = idx; break;
|
|
753
|
+
default: node = new Comment(this, ''); node.__idx = idx; break;
|
|
754
|
+
}
|
|
755
|
+
this.__cache[idx] = node;
|
|
756
|
+
return node;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Inflate an OWNED subtree from a nested parse tree (used by innerHTML= ).
|
|
760
|
+
__inflateNested(raw) {
|
|
761
|
+
let node;
|
|
762
|
+
switch (raw.nodeType) {
|
|
763
|
+
case ELEMENT_NODE:
|
|
764
|
+
node = new Element(this, raw.name, raw.namespace || '');
|
|
765
|
+
node.__attrs = raw.attrs.map((a) => ({ name: a.name, value: a.value, prefix: a.prefix || '' }));
|
|
766
|
+
if (raw.name === 'template') {
|
|
767
|
+
const contentRaw = raw.children.find((c) => c.nodeType === DOCUMENT_FRAGMENT_NODE && c.name === 'content');
|
|
768
|
+
if (contentRaw) { node.content = this.__inflateNested(contentRaw); }
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
case TEXT_NODE: node = new Text(this, raw.value); break;
|
|
772
|
+
case COMMENT_NODE: node = new Comment(this, raw.value); break;
|
|
773
|
+
case DOCUMENT_TYPE_NODE: node = new DocumentType(this, raw.name, raw.publicId, raw.systemId); break;
|
|
774
|
+
case DOCUMENT_FRAGMENT_NODE: node = new DocumentFragment(this); break;
|
|
775
|
+
default: node = new Comment(this, ''); break;
|
|
776
|
+
}
|
|
777
|
+
if (raw.nodeType !== TEXT_NODE && raw.nodeType !== COMMENT_NODE && raw.nodeType !== DOCUMENT_TYPE_NODE) {
|
|
778
|
+
const kids = [];
|
|
779
|
+
for (const rc of raw.children) {
|
|
780
|
+
if (rc.nodeType === DOCUMENT_FRAGMENT_NODE && rc.name === 'content') continue;
|
|
781
|
+
const child = this.__inflateNested(rc);
|
|
782
|
+
child.parentNode = node;
|
|
783
|
+
kids.push(child);
|
|
784
|
+
}
|
|
785
|
+
node.__kids = kids;
|
|
786
|
+
}
|
|
787
|
+
return node;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Layer 5: (re)point at the SoA buffer; children inflate lazily. Arena reset.
|
|
791
|
+
__load(soa) {
|
|
792
|
+
this.__buf = new Buffer(soa);
|
|
793
|
+
this.__idx = 0; // node 0 is the document
|
|
794
|
+
this.__kids = null; // drop overlay
|
|
795
|
+
this.__cache = []; // drop node cache
|
|
796
|
+
this.__active = null;
|
|
797
|
+
this.__mo = []; // drop observers
|
|
798
|
+
this.__moPending = null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
get documentElement() { return this.__children().find((n) => n.nodeType === ELEMENT_NODE && n.localName === 'html') ?? null; }
|
|
802
|
+
get doctype() { return this.__children().find((n) => n.nodeType === DOCUMENT_TYPE_NODE) ?? null; }
|
|
803
|
+
get head() { const html = this.documentElement; return html ? html.__children().find((n) => n.localName === 'head') ?? null : null; }
|
|
804
|
+
get body() { const html = this.documentElement; return html ? html.__children().find((n) => n.localName === 'body') ?? null : null; }
|
|
805
|
+
get activeElement() { return this.__active || this.body || null; }
|
|
806
|
+
__setActive(el) { this.__active = el; }
|
|
807
|
+
|
|
808
|
+
// ---- factories (owned nodes, no buffer) ----
|
|
809
|
+
createElement(tag) { return new Element(this, String(tag).toLowerCase(), ''); }
|
|
810
|
+
createElementNS(ns, qualified) {
|
|
811
|
+
const short = ns === SVG_NS ? 'svg' : ns === MATHML_NS ? 'math' : '';
|
|
812
|
+
const local = qualified.includes(':') ? qualified.split(':')[1] : qualified;
|
|
813
|
+
return new Element(this, local, short);
|
|
814
|
+
}
|
|
815
|
+
createTextNode(data) { return new Text(this, String(data)); }
|
|
816
|
+
createComment(data) { return new Comment(this, String(data)); }
|
|
817
|
+
createDocumentFragment() { return new DocumentFragment(this); }
|
|
818
|
+
createEvent() { return new Event(''); }
|
|
819
|
+
createRange() { return new Range(this); }
|
|
820
|
+
getSelection() { if (!this.__selection) this.__selection = makeSelection(); return this.__selection; }
|
|
821
|
+
importNode(node, deep) { return node.cloneNode(deep); }
|
|
822
|
+
adoptNode(node) { if (node.parentNode) node.parentNode.removeChild(node); node.ownerDocument = this; return node; }
|
|
823
|
+
|
|
824
|
+
// ---- queries ----
|
|
825
|
+
getElementById(id) {
|
|
826
|
+
let found = null;
|
|
827
|
+
const visit = (node) => {
|
|
828
|
+
for (const c of node.__children()) {
|
|
829
|
+
if (found) return;
|
|
830
|
+
if (c.nodeType === ELEMENT_NODE) { if (c.getAttribute('id') === id) { found = c; return; } visit(c); }
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
visit(this);
|
|
834
|
+
return found;
|
|
835
|
+
}
|
|
836
|
+
querySelector(sel) { return qsel(this, sel); }
|
|
837
|
+
querySelectorAll(sel) { return qselAll(this, sel); }
|
|
838
|
+
getElementsByTagName(tag) { const self = this; return liveHTMLCollection(() => collectByTag(self, tag.toLowerCase())); }
|
|
839
|
+
getElementsByClassName(cls) { const self = this; const classes = cls.split(/\s+/).filter(Boolean); return liveHTMLCollection(() => collectByClass(self, classes)); }
|
|
840
|
+
contains(node) { return Node.prototype.contains.call(this, node); }
|
|
841
|
+
|
|
842
|
+
get cookie() { return this.__cookie || ''; }
|
|
843
|
+
set cookie(v) { this.__cookie = (this.__cookie ? this.__cookie + '; ' : '') + v; }
|
|
844
|
+
|
|
845
|
+
// document state (honest defaults for a headless, focused, loaded page)
|
|
846
|
+
get visibilityState() { return 'visible'; }
|
|
847
|
+
get hidden() { return false; }
|
|
848
|
+
get readyState() { return 'complete'; }
|
|
849
|
+
hasFocus() { return true; }
|
|
850
|
+
get characterSet() { return 'UTF-8'; }
|
|
851
|
+
get compatMode() { return 'CSS1Compat'; }
|
|
852
|
+
get __owner() { return this.defaultView; }
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export { Event, CustomEvent };
|
|
856
|
+
|
|
857
|
+
// Parse an HTML string into a fresh Document over the immutable SoA buffer.
|
|
858
|
+
export function parseDocument(html) {
|
|
859
|
+
const soa = native.parseBuffer(String(html));
|
|
860
|
+
const doc = new Document();
|
|
861
|
+
doc.__load(soa);
|
|
862
|
+
return doc;
|
|
863
|
+
}
|