@derivesome/tree 0.1.1
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/.package.json.~undo-tree~ +4 -0
- package/.tsconfig.json.~undo-tree~ +4 -0
- package/dist/cjs/brands.d.ts +3 -0
- package/dist/cjs/brands.d.ts.map +1 -0
- package/dist/cjs/brands.js +5 -0
- package/dist/cjs/brands.js.map +1 -0
- package/dist/cjs/context.d.ts +28 -0
- package/dist/cjs/context.d.ts.map +1 -0
- package/dist/cjs/context.js +48 -0
- package/dist/cjs/context.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mount.d.ts +6 -0
- package/dist/cjs/mount.d.ts.map +1 -0
- package/dist/cjs/mount.js +209 -0
- package/dist/cjs/mount.js.map +1 -0
- package/dist/cjs/props.d.ts +12 -0
- package/dist/cjs/props.d.ts.map +1 -0
- package/dist/cjs/props.js +80 -0
- package/dist/cjs/props.js.map +1 -0
- package/dist/cjs/renderer.d.ts +29 -0
- package/dist/cjs/renderer.d.ts.map +1 -0
- package/dist/cjs/renderer.js +3 -0
- package/dist/cjs/renderer.js.map +1 -0
- package/dist/cjs/tree-node-like.d.ts +8 -0
- package/dist/cjs/tree-node-like.d.ts.map +1 -0
- package/dist/cjs/tree-node-like.js +4 -0
- package/dist/cjs/tree-node-like.js.map +1 -0
- package/dist/cjs/tree.d.ts +46 -0
- package/dist/cjs/tree.d.ts.map +1 -0
- package/dist/cjs/tree.js +154 -0
- package/dist/cjs/tree.js.map +1 -0
- package/dist/cjs/velement.d.ts +185 -0
- package/dist/cjs/velement.d.ts.map +1 -0
- package/dist/cjs/velement.js +874 -0
- package/dist/cjs/velement.js.map +1 -0
- package/dist/esm/brands.d.ts +3 -0
- package/dist/esm/brands.d.ts.map +1 -0
- package/dist/esm/brands.js +5 -0
- package/dist/esm/brands.js.map +1 -0
- package/dist/esm/context.d.ts +28 -0
- package/dist/esm/context.d.ts.map +1 -0
- package/dist/esm/context.js +48 -0
- package/dist/esm/context.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mount.d.ts +6 -0
- package/dist/esm/mount.d.ts.map +1 -0
- package/dist/esm/mount.js +209 -0
- package/dist/esm/mount.js.map +1 -0
- package/dist/esm/props.d.ts +12 -0
- package/dist/esm/props.d.ts.map +1 -0
- package/dist/esm/props.js +80 -0
- package/dist/esm/props.js.map +1 -0
- package/dist/esm/renderer.d.ts +29 -0
- package/dist/esm/renderer.d.ts.map +1 -0
- package/dist/esm/renderer.js +3 -0
- package/dist/esm/renderer.js.map +1 -0
- package/dist/esm/tree-node-like.d.ts +8 -0
- package/dist/esm/tree-node-like.d.ts.map +1 -0
- package/dist/esm/tree-node-like.js +4 -0
- package/dist/esm/tree-node-like.js.map +1 -0
- package/dist/esm/tree.d.ts +46 -0
- package/dist/esm/tree.d.ts.map +1 -0
- package/dist/esm/tree.js +154 -0
- package/dist/esm/tree.js.map +1 -0
- package/dist/esm/velement.d.ts +185 -0
- package/dist/esm/velement.d.ts.map +1 -0
- package/dist/esm/velement.js +874 -0
- package/dist/esm/velement.js.map +1 -0
- package/package.json +46 -0
- package/package.json~ +52 -0
- package/src/#mount.test.ts# +372 -0
- package/src/.brands.ts.~undo-tree~ +6 -0
- package/src/.context.ts.~undo-tree~ +6 -0
- package/src/.index.ts.~undo-tree~ +11 -0
- package/src/.mount.test.ts.~undo-tree~ +438 -0
- package/src/.mount.ts.~undo-tree~ +70 -0
- package/src/.node-like.ts.~undo-tree~ +8 -0
- package/src/.props.ts.~undo-tree~ +125 -0
- package/src/.renderer.ts.~undo-tree~ +18 -0
- package/src/.tree-node-like.ts.~undo-tree~ +12 -0
- package/src/.tree.ts.~undo-tree~ +46 -0
- package/src/.velement.ts.~undo-tree~ +1739 -0
- package/src/brands.ts +2 -0
- package/src/brands.ts~ +0 -0
- package/src/context.ts +61 -0
- package/src/context.ts~ +0 -0
- package/src/index.ts +5 -0
- package/src/index.ts~ +4 -0
- package/src/mount.test.ts +405 -0
- package/src/mount.test.ts~ +375 -0
- package/src/mount.ts +332 -0
- package/src/mount.ts~ +306 -0
- package/src/node-like.ts~ +0 -0
- package/src/props.ts +99 -0
- package/src/props.ts~ +86 -0
- package/src/renderer.ts +37 -0
- package/src/renderer.ts~ +37 -0
- package/src/tree-node-like.ts +8 -0
- package/src/tree-node-like.ts~ +6 -0
- package/src/tree.ts +226 -0
- package/src/tree.ts~ +227 -0
- package/src/velement.ts +990 -0
- package/src/velement.ts~ +966 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +23 -0
- package/tsconfig.json~ +23 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// virtual-dom.ts
|
|
3
|
+
// A lightweight virtual DOM implementation for unit testing browser code without a real browser.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.VEvent = exports.VDocument = exports.VDocumentFragment = exports.VElement = exports.VStyle = exports.VClassList = exports.VComment = exports.VText = exports.VNode = exports.NodeType = void 0;
|
|
6
|
+
exports.toHTML = toHTML;
|
|
7
|
+
exports.createDocument = createDocument;
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Node type constants (mirrors the real DOM)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
var NodeType;
|
|
12
|
+
(function (NodeType) {
|
|
13
|
+
NodeType[NodeType["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
|
|
14
|
+
NodeType[NodeType["TEXT_NODE"] = 3] = "TEXT_NODE";
|
|
15
|
+
NodeType[NodeType["COMMENT_NODE"] = 8] = "COMMENT_NODE";
|
|
16
|
+
NodeType[NodeType["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE";
|
|
17
|
+
NodeType[NodeType["DOCUMENT_TYPE_NODE"] = 10] = "DOCUMENT_TYPE_NODE";
|
|
18
|
+
NodeType[NodeType["DOCUMENT_FRAGMENT_NODE"] = 11] = "DOCUMENT_FRAGMENT_NODE";
|
|
19
|
+
})(NodeType || (exports.NodeType = NodeType = {}));
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// VNode — abstract base for every node
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
class VNode {
|
|
24
|
+
parentNode = null;
|
|
25
|
+
childNodes = [];
|
|
26
|
+
ownerDocument = null;
|
|
27
|
+
get firstChild() {
|
|
28
|
+
return this.childNodes[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
get lastChild() {
|
|
31
|
+
return this.childNodes[this.childNodes.length - 1] ?? null;
|
|
32
|
+
}
|
|
33
|
+
get nextSibling() {
|
|
34
|
+
if (!this.parentNode)
|
|
35
|
+
return null;
|
|
36
|
+
const siblings = this.parentNode.childNodes;
|
|
37
|
+
const idx = siblings.indexOf(this);
|
|
38
|
+
return siblings[idx + 1] ?? null;
|
|
39
|
+
}
|
|
40
|
+
get previousSibling() {
|
|
41
|
+
if (!this.parentNode)
|
|
42
|
+
return null;
|
|
43
|
+
const siblings = this.parentNode.childNodes;
|
|
44
|
+
const idx = siblings.indexOf(this);
|
|
45
|
+
return idx > 0 ? (siblings[idx - 1] ?? null) : null;
|
|
46
|
+
}
|
|
47
|
+
get parentElement() {
|
|
48
|
+
return this.parentNode instanceof VElement ? this.parentNode : null;
|
|
49
|
+
}
|
|
50
|
+
// ---- Mutation helpers ----
|
|
51
|
+
appendChild(child) {
|
|
52
|
+
this._removeFromParent(child);
|
|
53
|
+
child.parentNode = this;
|
|
54
|
+
child.ownerDocument = this.ownerDocument;
|
|
55
|
+
this.childNodes.push(child);
|
|
56
|
+
return child;
|
|
57
|
+
}
|
|
58
|
+
insertBefore(newChild, refChild) {
|
|
59
|
+
if (refChild === null)
|
|
60
|
+
return this.appendChild(newChild);
|
|
61
|
+
const idx = this.childNodes.indexOf(refChild);
|
|
62
|
+
if (idx === -1)
|
|
63
|
+
throw new Error("refChild is not a child of this node");
|
|
64
|
+
this._removeFromParent(newChild);
|
|
65
|
+
newChild.parentNode = this;
|
|
66
|
+
newChild.ownerDocument = this.ownerDocument;
|
|
67
|
+
this.childNodes.splice(idx, 0, newChild);
|
|
68
|
+
return newChild;
|
|
69
|
+
}
|
|
70
|
+
removeChild(child) {
|
|
71
|
+
const idx = this.childNodes.indexOf(child);
|
|
72
|
+
if (idx === -1)
|
|
73
|
+
throw new Error("Node is not a child of this node");
|
|
74
|
+
this.childNodes.splice(idx, 1);
|
|
75
|
+
child.parentNode = null;
|
|
76
|
+
return child;
|
|
77
|
+
}
|
|
78
|
+
replaceChild(newChild, oldChild) {
|
|
79
|
+
const idx = this.childNodes.indexOf(oldChild);
|
|
80
|
+
if (idx === -1)
|
|
81
|
+
throw new Error("oldChild is not a child of this node");
|
|
82
|
+
this._removeFromParent(newChild);
|
|
83
|
+
newChild.parentNode = this;
|
|
84
|
+
newChild.ownerDocument = this.ownerDocument;
|
|
85
|
+
this.childNodes.splice(idx, 1, newChild);
|
|
86
|
+
oldChild.parentNode = null;
|
|
87
|
+
return oldChild;
|
|
88
|
+
}
|
|
89
|
+
hasChildNodes() {
|
|
90
|
+
return this.childNodes.length > 0;
|
|
91
|
+
}
|
|
92
|
+
contains(other) {
|
|
93
|
+
if (!other)
|
|
94
|
+
return false;
|
|
95
|
+
let current = other;
|
|
96
|
+
while (current) {
|
|
97
|
+
if (current === this)
|
|
98
|
+
return true;
|
|
99
|
+
current = current.parentNode;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
cloneNode(deep = false) {
|
|
104
|
+
return this._clone(deep);
|
|
105
|
+
}
|
|
106
|
+
/** Remove a node from its current parent before re-parenting. */
|
|
107
|
+
_removeFromParent(child) {
|
|
108
|
+
if (child.parentNode) {
|
|
109
|
+
child.parentNode.removeChild(child);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.VNode = VNode;
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// VText
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
class VText extends VNode {
|
|
118
|
+
data;
|
|
119
|
+
nodeType = NodeType.TEXT_NODE;
|
|
120
|
+
nodeName = "#text";
|
|
121
|
+
constructor(data) {
|
|
122
|
+
super();
|
|
123
|
+
this.data = data;
|
|
124
|
+
}
|
|
125
|
+
get textContent() {
|
|
126
|
+
return this.data;
|
|
127
|
+
}
|
|
128
|
+
set textContent(value) {
|
|
129
|
+
this.data = value ?? "";
|
|
130
|
+
}
|
|
131
|
+
get nodeValue() {
|
|
132
|
+
return this.data;
|
|
133
|
+
}
|
|
134
|
+
set nodeValue(value) {
|
|
135
|
+
this.data = value ?? "";
|
|
136
|
+
}
|
|
137
|
+
_clone(_deep) {
|
|
138
|
+
return new VText(this.data);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.VText = VText;
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// VComment
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
class VComment extends VNode {
|
|
146
|
+
data;
|
|
147
|
+
nodeType = NodeType.COMMENT_NODE;
|
|
148
|
+
nodeName = "#comment";
|
|
149
|
+
constructor(data) {
|
|
150
|
+
super();
|
|
151
|
+
this.data = data;
|
|
152
|
+
}
|
|
153
|
+
get textContent() {
|
|
154
|
+
return this.data;
|
|
155
|
+
}
|
|
156
|
+
set textContent(value) {
|
|
157
|
+
this.data = value ?? "";
|
|
158
|
+
}
|
|
159
|
+
get nodeValue() {
|
|
160
|
+
return this.data;
|
|
161
|
+
}
|
|
162
|
+
set nodeValue(value) {
|
|
163
|
+
this.data = value ?? "";
|
|
164
|
+
}
|
|
165
|
+
_clone(_deep) {
|
|
166
|
+
return new VComment(this.data);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.VComment = VComment;
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Attr map — case-insensitive for HTML-like behaviour
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
class AttributeMap {
|
|
174
|
+
_map = new Map();
|
|
175
|
+
get(name) {
|
|
176
|
+
return this._map.get(name.toLowerCase()) ?? null;
|
|
177
|
+
}
|
|
178
|
+
set(name, value) {
|
|
179
|
+
this._map.set(name.toLowerCase(), value);
|
|
180
|
+
}
|
|
181
|
+
has(name) {
|
|
182
|
+
return this._map.has(name.toLowerCase());
|
|
183
|
+
}
|
|
184
|
+
remove(name) {
|
|
185
|
+
return this._map.delete(name.toLowerCase());
|
|
186
|
+
}
|
|
187
|
+
entries() {
|
|
188
|
+
return this._map.entries();
|
|
189
|
+
}
|
|
190
|
+
get size() {
|
|
191
|
+
return this._map.size;
|
|
192
|
+
}
|
|
193
|
+
clone() {
|
|
194
|
+
const copy = new AttributeMap();
|
|
195
|
+
for (const [k, v] of this._map)
|
|
196
|
+
copy._map.set(k, v);
|
|
197
|
+
return copy;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// ClassList — minimal DOMTokenList stand-in
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
class VClassList {
|
|
204
|
+
_el;
|
|
205
|
+
constructor(_el) {
|
|
206
|
+
this._el = _el;
|
|
207
|
+
}
|
|
208
|
+
_tokens() {
|
|
209
|
+
const raw = this._el.getAttribute("class") ?? "";
|
|
210
|
+
return raw.split(/\s+/).filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
_write(tokens) {
|
|
213
|
+
this._el.setAttribute("class", tokens.join(" "));
|
|
214
|
+
}
|
|
215
|
+
add(...tokens) {
|
|
216
|
+
const set = new Set(this._tokens());
|
|
217
|
+
for (const t of tokens)
|
|
218
|
+
set.add(t);
|
|
219
|
+
this._write([...set]);
|
|
220
|
+
}
|
|
221
|
+
remove(...tokens) {
|
|
222
|
+
const set = new Set(this._tokens());
|
|
223
|
+
for (const t of tokens)
|
|
224
|
+
set.delete(t);
|
|
225
|
+
this._write([...set]);
|
|
226
|
+
}
|
|
227
|
+
toggle(token, force) {
|
|
228
|
+
const set = new Set(this._tokens());
|
|
229
|
+
const shouldAdd = force !== undefined ? force : !set.has(token);
|
|
230
|
+
if (shouldAdd) {
|
|
231
|
+
set.add(token);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
set.delete(token);
|
|
235
|
+
}
|
|
236
|
+
this._write([...set]);
|
|
237
|
+
return shouldAdd;
|
|
238
|
+
}
|
|
239
|
+
contains(token) {
|
|
240
|
+
return this._tokens().includes(token);
|
|
241
|
+
}
|
|
242
|
+
replace(oldToken, newToken) {
|
|
243
|
+
const tokens = this._tokens();
|
|
244
|
+
const idx = tokens.indexOf(oldToken);
|
|
245
|
+
if (idx === -1)
|
|
246
|
+
return false;
|
|
247
|
+
tokens.splice(idx, 1, newToken);
|
|
248
|
+
this._write(tokens);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
get length() {
|
|
252
|
+
return this._tokens().length;
|
|
253
|
+
}
|
|
254
|
+
toString() {
|
|
255
|
+
return this._tokens().join(" ");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
exports.VClassList = VClassList;
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Style — minimal CSSStyleDeclaration stand-in
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
class VStyle {
|
|
263
|
+
// @ts-ignore
|
|
264
|
+
_props = new Map();
|
|
265
|
+
getPropertyValue(name) {
|
|
266
|
+
return this._props.get(name) ?? "";
|
|
267
|
+
}
|
|
268
|
+
setProperty(name, value) {
|
|
269
|
+
if (value === null || value === "") {
|
|
270
|
+
this._props.delete(name);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this._props.set(name, value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
removeProperty(name) {
|
|
277
|
+
const old = this._props.get(name) ?? "";
|
|
278
|
+
this._props.delete(name);
|
|
279
|
+
return old;
|
|
280
|
+
}
|
|
281
|
+
get cssText() {
|
|
282
|
+
return [...this._props.entries()].map(([k, v]) => `${k}: ${v};`).join(" ");
|
|
283
|
+
}
|
|
284
|
+
set cssText(value) {
|
|
285
|
+
this._props.clear();
|
|
286
|
+
if (!value)
|
|
287
|
+
return;
|
|
288
|
+
for (const decl of value.split(";")) {
|
|
289
|
+
const colon = decl.indexOf(":");
|
|
290
|
+
if (colon === -1)
|
|
291
|
+
continue;
|
|
292
|
+
const prop = decl.slice(0, colon).trim();
|
|
293
|
+
const val = decl.slice(colon + 1).trim();
|
|
294
|
+
if (prop)
|
|
295
|
+
this._props.set(prop, val);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
exports.VStyle = VStyle;
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// VElement
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
class VElement extends VNode {
|
|
304
|
+
nodeType = NodeType.ELEMENT_NODE;
|
|
305
|
+
tagName;
|
|
306
|
+
classList;
|
|
307
|
+
style;
|
|
308
|
+
_attrs = new AttributeMap();
|
|
309
|
+
_eventListeners = new Map();
|
|
310
|
+
_dataset = {};
|
|
311
|
+
constructor(tagName) {
|
|
312
|
+
super();
|
|
313
|
+
this.tagName = tagName.toUpperCase();
|
|
314
|
+
this.classList = new VClassList(this);
|
|
315
|
+
this.style = new VStyle();
|
|
316
|
+
// Return a proxy so element.id, element.className etc. work naturally
|
|
317
|
+
return new Proxy(this, {
|
|
318
|
+
get(target, prop, receiver) {
|
|
319
|
+
// Built-in properties first
|
|
320
|
+
if (prop in target || typeof prop === "symbol") {
|
|
321
|
+
return Reflect.get(target, prop, receiver);
|
|
322
|
+
}
|
|
323
|
+
// Fallback: treat as an attribute shorthand (e.g. el.id, el.href)
|
|
324
|
+
if (typeof prop === "string") {
|
|
325
|
+
return target.getAttribute(prop) ?? undefined;
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
},
|
|
329
|
+
set(target, prop, value, receiver) {
|
|
330
|
+
if (prop in target || typeof prop === "symbol") {
|
|
331
|
+
return Reflect.set(target, prop, value, receiver);
|
|
332
|
+
}
|
|
333
|
+
if (typeof prop === "string") {
|
|
334
|
+
target.setAttribute(prop, String(value));
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
get nodeName() {
|
|
342
|
+
return this.tagName;
|
|
343
|
+
}
|
|
344
|
+
// ---- id / className shortcuts ----
|
|
345
|
+
get id() {
|
|
346
|
+
return this.getAttribute("id") ?? "";
|
|
347
|
+
}
|
|
348
|
+
set id(value) {
|
|
349
|
+
this.setAttribute("id", value);
|
|
350
|
+
}
|
|
351
|
+
get className() {
|
|
352
|
+
return this.getAttribute("class") ?? "";
|
|
353
|
+
}
|
|
354
|
+
set className(value) {
|
|
355
|
+
this.setAttribute("class", value);
|
|
356
|
+
}
|
|
357
|
+
// ---- dataset ----
|
|
358
|
+
get dataset() {
|
|
359
|
+
return this._dataset;
|
|
360
|
+
}
|
|
361
|
+
// ---- textContent / innerHTML ----
|
|
362
|
+
get textContent() {
|
|
363
|
+
return this.childNodes.map((c) => c.textContent ?? "").join("");
|
|
364
|
+
}
|
|
365
|
+
set textContent(value) {
|
|
366
|
+
this.childNodes = [];
|
|
367
|
+
if (value) {
|
|
368
|
+
this.appendChild(new VText(value));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
get innerHTML() {
|
|
372
|
+
return this.childNodes.map((c) => serializeNode(c)).join("");
|
|
373
|
+
}
|
|
374
|
+
set innerHTML(html) {
|
|
375
|
+
// Minimal: just set the raw text. A real implementation would parse HTML.
|
|
376
|
+
this.childNodes = [];
|
|
377
|
+
if (html) {
|
|
378
|
+
this.appendChild(new VText(html));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
get outerHTML() {
|
|
382
|
+
return serializeNode(this);
|
|
383
|
+
}
|
|
384
|
+
// ---- Attributes ----
|
|
385
|
+
getAttribute(name) {
|
|
386
|
+
return this._attrs.get(name);
|
|
387
|
+
}
|
|
388
|
+
setAttribute(name, value) {
|
|
389
|
+
this._attrs.set(name, value);
|
|
390
|
+
// Keep dataset in sync
|
|
391
|
+
if (name.startsWith("data-")) {
|
|
392
|
+
this._dataset[camelCase(name.slice(5))] = value;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
removeAttribute(name) {
|
|
396
|
+
this._attrs.remove(name);
|
|
397
|
+
if (name.startsWith("data-")) {
|
|
398
|
+
delete this._dataset[camelCase(name.slice(5))];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
hasAttribute(name) {
|
|
402
|
+
return this._attrs.has(name);
|
|
403
|
+
}
|
|
404
|
+
getAttributeNames() {
|
|
405
|
+
return [...this._attrs.entries()].map(([k]) => k);
|
|
406
|
+
}
|
|
407
|
+
// ---- Query helpers ----
|
|
408
|
+
getElementById(id) {
|
|
409
|
+
return this._query((el) => el.getAttribute("id") === id);
|
|
410
|
+
}
|
|
411
|
+
getElementsByClassName(className) {
|
|
412
|
+
const target = className.trim();
|
|
413
|
+
return this._queryAll((el) => el.classList.contains(target));
|
|
414
|
+
}
|
|
415
|
+
getElementsByTagName(tag) {
|
|
416
|
+
const upper = tag.toUpperCase();
|
|
417
|
+
return this._queryAll((el) => upper === "*" || el.tagName === upper);
|
|
418
|
+
}
|
|
419
|
+
querySelector(selector) {
|
|
420
|
+
return this._query(makeSimpleMatcher(selector));
|
|
421
|
+
}
|
|
422
|
+
querySelectorAll(selector) {
|
|
423
|
+
return this._queryAll(makeSimpleMatcher(selector));
|
|
424
|
+
}
|
|
425
|
+
closest(selector) {
|
|
426
|
+
const matcher = makeSimpleMatcher(selector);
|
|
427
|
+
let current = this;
|
|
428
|
+
while (current) {
|
|
429
|
+
if (current instanceof VElement && matcher(current)) {
|
|
430
|
+
return current;
|
|
431
|
+
}
|
|
432
|
+
current = current.parentNode;
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
matches(selector) {
|
|
437
|
+
return makeSimpleMatcher(selector)(this);
|
|
438
|
+
}
|
|
439
|
+
// ---- Events (record & replay for testing) ----
|
|
440
|
+
addEventListener(type, listener) {
|
|
441
|
+
let list = this._eventListeners.get(type);
|
|
442
|
+
if (!list) {
|
|
443
|
+
list = [];
|
|
444
|
+
this._eventListeners.set(type, list);
|
|
445
|
+
}
|
|
446
|
+
list.push(listener);
|
|
447
|
+
}
|
|
448
|
+
removeEventListener(type, listener) {
|
|
449
|
+
const arr = this._eventListeners.get(type);
|
|
450
|
+
if (!arr)
|
|
451
|
+
return;
|
|
452
|
+
const idx = arr.indexOf(listener);
|
|
453
|
+
if (idx !== -1)
|
|
454
|
+
arr.splice(idx, 1);
|
|
455
|
+
}
|
|
456
|
+
dispatchEvent(event) {
|
|
457
|
+
const listeners = this._eventListeners.get(event.type) ?? [];
|
|
458
|
+
for (const l of listeners) {
|
|
459
|
+
if (typeof l === "function")
|
|
460
|
+
l(event);
|
|
461
|
+
else
|
|
462
|
+
l.handleEvent(event);
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
/** Click shorthand — dispatches a "click" VEvent. */
|
|
467
|
+
click() {
|
|
468
|
+
this.dispatchEvent(new VEvent("click"));
|
|
469
|
+
}
|
|
470
|
+
// ---- Children helpers ----
|
|
471
|
+
get children() {
|
|
472
|
+
return this.childNodes.filter((c) => c instanceof VElement);
|
|
473
|
+
}
|
|
474
|
+
get childElementCount() {
|
|
475
|
+
return this.children.length;
|
|
476
|
+
}
|
|
477
|
+
get firstElementChild() {
|
|
478
|
+
return this.children[0] ?? null;
|
|
479
|
+
}
|
|
480
|
+
get lastElementChild() {
|
|
481
|
+
const ch = this.children;
|
|
482
|
+
return ch[ch.length - 1] ?? null;
|
|
483
|
+
}
|
|
484
|
+
// ---- Clone ----
|
|
485
|
+
_clone(deep) {
|
|
486
|
+
const el = new VElement(this.tagName);
|
|
487
|
+
el._attrs = this._attrs.clone();
|
|
488
|
+
el._dataset = { ...this._dataset };
|
|
489
|
+
el.style.cssText = this.style.cssText;
|
|
490
|
+
if (deep) {
|
|
491
|
+
for (const child of this.childNodes) {
|
|
492
|
+
el.appendChild(child.cloneNode(true));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return el;
|
|
496
|
+
}
|
|
497
|
+
// ---- Internal query helpers ----
|
|
498
|
+
_query(pred) {
|
|
499
|
+
for (const child of this.childNodes) {
|
|
500
|
+
if (child instanceof VElement) {
|
|
501
|
+
if (pred(child))
|
|
502
|
+
return child;
|
|
503
|
+
const found = child._query(pred);
|
|
504
|
+
if (found)
|
|
505
|
+
return found;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
_queryAll(pred) {
|
|
511
|
+
const results = [];
|
|
512
|
+
for (const child of this.childNodes) {
|
|
513
|
+
if (child instanceof VElement) {
|
|
514
|
+
if (pred(child))
|
|
515
|
+
results.push(child);
|
|
516
|
+
results.push(...child._queryAll(pred));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return results;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
exports.VElement = VElement;
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// VDocumentFragment
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
class VDocumentFragment extends VNode {
|
|
527
|
+
nodeType = NodeType.DOCUMENT_FRAGMENT_NODE;
|
|
528
|
+
nodeName = "#document-fragment";
|
|
529
|
+
get textContent() {
|
|
530
|
+
return this.childNodes.map((c) => c.textContent ?? "").join("");
|
|
531
|
+
}
|
|
532
|
+
set textContent(value) {
|
|
533
|
+
this.childNodes = [];
|
|
534
|
+
if (value)
|
|
535
|
+
this.appendChild(new VText(value));
|
|
536
|
+
}
|
|
537
|
+
_clone(deep) {
|
|
538
|
+
const frag = new VDocumentFragment();
|
|
539
|
+
if (deep) {
|
|
540
|
+
for (const child of this.childNodes) {
|
|
541
|
+
frag.appendChild(child.cloneNode(true));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return frag;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
exports.VDocumentFragment = VDocumentFragment;
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// VDocument
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
class VDocument extends VNode {
|
|
552
|
+
nodeType = NodeType.DOCUMENT_NODE;
|
|
553
|
+
nodeName = "#document";
|
|
554
|
+
body;
|
|
555
|
+
head;
|
|
556
|
+
documentElement;
|
|
557
|
+
constructor() {
|
|
558
|
+
super();
|
|
559
|
+
this.ownerDocument = this;
|
|
560
|
+
this.documentElement = this._make("html");
|
|
561
|
+
this.head = this._make("head");
|
|
562
|
+
this.body = this._make("body");
|
|
563
|
+
this.documentElement.appendChild(this.head);
|
|
564
|
+
this.documentElement.appendChild(this.body);
|
|
565
|
+
this.appendChild(this.documentElement);
|
|
566
|
+
}
|
|
567
|
+
get textContent() {
|
|
568
|
+
return null; // matches real DOM behaviour
|
|
569
|
+
}
|
|
570
|
+
set textContent(_value) {
|
|
571
|
+
// no-op for Document, matches real DOM
|
|
572
|
+
}
|
|
573
|
+
createElement(tagName) {
|
|
574
|
+
const el = new VElement(tagName);
|
|
575
|
+
el.ownerDocument = this;
|
|
576
|
+
return el;
|
|
577
|
+
}
|
|
578
|
+
createTextNode(data) {
|
|
579
|
+
const t = new VText(data);
|
|
580
|
+
t.ownerDocument = this;
|
|
581
|
+
return t;
|
|
582
|
+
}
|
|
583
|
+
createComment(data) {
|
|
584
|
+
const c = new VComment(data);
|
|
585
|
+
c.ownerDocument = this;
|
|
586
|
+
return c;
|
|
587
|
+
}
|
|
588
|
+
createDocumentFragment() {
|
|
589
|
+
const f = new VDocumentFragment();
|
|
590
|
+
f.ownerDocument = this;
|
|
591
|
+
return f;
|
|
592
|
+
}
|
|
593
|
+
getElementById(id) {
|
|
594
|
+
return this.documentElement.getElementById(id);
|
|
595
|
+
}
|
|
596
|
+
getElementsByClassName(className) {
|
|
597
|
+
return this.documentElement.getElementsByClassName(className);
|
|
598
|
+
}
|
|
599
|
+
getElementsByTagName(tag) {
|
|
600
|
+
return this.documentElement.getElementsByTagName(tag);
|
|
601
|
+
}
|
|
602
|
+
querySelector(selector) {
|
|
603
|
+
return this.documentElement.querySelector(selector);
|
|
604
|
+
}
|
|
605
|
+
querySelectorAll(selector) {
|
|
606
|
+
return this.documentElement.querySelectorAll(selector);
|
|
607
|
+
}
|
|
608
|
+
_clone(deep) {
|
|
609
|
+
const doc = new VDocument();
|
|
610
|
+
if (deep) {
|
|
611
|
+
// Already has html > head + body structure; skip re-creating
|
|
612
|
+
}
|
|
613
|
+
return doc;
|
|
614
|
+
}
|
|
615
|
+
_make(tag) {
|
|
616
|
+
const el = new VElement(tag);
|
|
617
|
+
el.ownerDocument = this;
|
|
618
|
+
return el;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
exports.VDocument = VDocument;
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
// VEvent — lightweight Event stand-in
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
class VEvent {
|
|
626
|
+
type;
|
|
627
|
+
bubbles;
|
|
628
|
+
cancelable;
|
|
629
|
+
defaultPrevented = false;
|
|
630
|
+
target = null;
|
|
631
|
+
currentTarget = null;
|
|
632
|
+
constructor(type, opts = {}) {
|
|
633
|
+
this.type = type;
|
|
634
|
+
this.bubbles = opts.bubbles ?? false;
|
|
635
|
+
this.cancelable = opts.cancelable ?? false;
|
|
636
|
+
}
|
|
637
|
+
preventDefault() {
|
|
638
|
+
if (this.cancelable)
|
|
639
|
+
this.defaultPrevented = true;
|
|
640
|
+
}
|
|
641
|
+
stopPropagation() {
|
|
642
|
+
/* noop for now */
|
|
643
|
+
}
|
|
644
|
+
stopImmediatePropagation() {
|
|
645
|
+
/* noop for now */
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
exports.VEvent = VEvent;
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Serialization helper (outerHTML / innerHTML / toHTML)
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
const VOID_ELEMENTS = new Set([
|
|
653
|
+
"AREA",
|
|
654
|
+
"BASE",
|
|
655
|
+
"BR",
|
|
656
|
+
"COL",
|
|
657
|
+
"EMBED",
|
|
658
|
+
"HR",
|
|
659
|
+
"IMG",
|
|
660
|
+
"INPUT",
|
|
661
|
+
"LINK",
|
|
662
|
+
"META",
|
|
663
|
+
"PARAM",
|
|
664
|
+
"SOURCE",
|
|
665
|
+
"TRACK",
|
|
666
|
+
"WBR",
|
|
667
|
+
]);
|
|
668
|
+
/** Flat serialization used by innerHTML / outerHTML (no indentation). */
|
|
669
|
+
function serializeNode(node) {
|
|
670
|
+
if (node instanceof VText)
|
|
671
|
+
return escapeHtml(node.data);
|
|
672
|
+
if (node instanceof VComment)
|
|
673
|
+
return `<!--${node.data}-->`;
|
|
674
|
+
if (node instanceof VElement) {
|
|
675
|
+
const tag = node.tagName.toLowerCase();
|
|
676
|
+
let attrs = "";
|
|
677
|
+
for (const name of node.getAttributeNames()) {
|
|
678
|
+
const val = node.getAttribute(name) ?? "";
|
|
679
|
+
attrs += ` ${name}="${escapeAttr(val)}"`;
|
|
680
|
+
}
|
|
681
|
+
if (VOID_ELEMENTS.has(node.tagName)) {
|
|
682
|
+
return `<${tag}${attrs} />`;
|
|
683
|
+
}
|
|
684
|
+
const inner = node.childNodes.map(serializeNode).join("");
|
|
685
|
+
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
686
|
+
}
|
|
687
|
+
if (node instanceof VDocumentFragment) {
|
|
688
|
+
return node.childNodes.map(serializeNode).join("");
|
|
689
|
+
}
|
|
690
|
+
if (node instanceof VDocument) {
|
|
691
|
+
return "<!DOCTYPE html>\n" + node.childNodes.map(serializeNode).join("");
|
|
692
|
+
}
|
|
693
|
+
return "";
|
|
694
|
+
}
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
// Pretty-printed serialization (used by toHTML)
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
/** Elements whose content is purely inline and should stay on one line. */
|
|
699
|
+
const INLINE_ELEMENTS = new Set([
|
|
700
|
+
"A",
|
|
701
|
+
"ABBR",
|
|
702
|
+
"B",
|
|
703
|
+
"BDO",
|
|
704
|
+
"BR",
|
|
705
|
+
"CITE",
|
|
706
|
+
"CODE",
|
|
707
|
+
"DFN",
|
|
708
|
+
"EM",
|
|
709
|
+
"I",
|
|
710
|
+
"KBD",
|
|
711
|
+
"LABEL",
|
|
712
|
+
"MAP",
|
|
713
|
+
"Q",
|
|
714
|
+
"S",
|
|
715
|
+
"SAMP",
|
|
716
|
+
"SMALL",
|
|
717
|
+
"SPAN",
|
|
718
|
+
"STRONG",
|
|
719
|
+
"SUB",
|
|
720
|
+
"SUP",
|
|
721
|
+
"TEXTAREA",
|
|
722
|
+
"TIME",
|
|
723
|
+
"U",
|
|
724
|
+
"VAR",
|
|
725
|
+
]);
|
|
726
|
+
/**
|
|
727
|
+
* Returns `true` when every child is either a text node or an inline element
|
|
728
|
+
* that itself contains only text / inline children. In that case we render
|
|
729
|
+
* the element on a single line to avoid awkward whitespace inside e.g.
|
|
730
|
+
* `<p>Hello <b>world</b></p>`.
|
|
731
|
+
*/
|
|
732
|
+
function isInlineContent(node) {
|
|
733
|
+
return node.childNodes.every((child) => {
|
|
734
|
+
if (child instanceof VText)
|
|
735
|
+
return true;
|
|
736
|
+
if (child instanceof VElement && INLINE_ELEMENTS.has(child.tagName)) {
|
|
737
|
+
return isInlineContent(child);
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
function prettyPrint(node, depth, indent) {
|
|
743
|
+
const pad = indent.repeat(depth);
|
|
744
|
+
if (node instanceof VText) {
|
|
745
|
+
const trimmed = node.data.trim();
|
|
746
|
+
return trimmed.length > 0 ? pad + escapeHtml(trimmed) : "";
|
|
747
|
+
}
|
|
748
|
+
if (node instanceof VComment) {
|
|
749
|
+
return `${pad}<!--${node.data}-->`;
|
|
750
|
+
}
|
|
751
|
+
if (node instanceof VElement) {
|
|
752
|
+
const tag = node.tagName.toLowerCase();
|
|
753
|
+
let attrs = "";
|
|
754
|
+
for (const name of node.getAttributeNames()) {
|
|
755
|
+
const val = node.getAttribute(name) ?? "";
|
|
756
|
+
attrs += ` ${name}="${escapeAttr(val)}"`;
|
|
757
|
+
}
|
|
758
|
+
// Self-closing / void
|
|
759
|
+
if (VOID_ELEMENTS.has(node.tagName)) {
|
|
760
|
+
return `${pad}<${tag}${attrs} />`;
|
|
761
|
+
}
|
|
762
|
+
// No children
|
|
763
|
+
if (node.childNodes.length === 0) {
|
|
764
|
+
return `${pad}<${tag}${attrs}></${tag}>`;
|
|
765
|
+
}
|
|
766
|
+
// Inline content — keep on one line
|
|
767
|
+
if (isInlineContent(node)) {
|
|
768
|
+
const inner = node.childNodes.map((c) => serializeNode(c)).join("");
|
|
769
|
+
return `${pad}<${tag}${attrs}>${inner}</${tag}>`;
|
|
770
|
+
}
|
|
771
|
+
// Block content — children on separate indented lines
|
|
772
|
+
const children = node.childNodes
|
|
773
|
+
.map((c) => prettyPrint(c, depth + 1, indent))
|
|
774
|
+
.filter(Boolean)
|
|
775
|
+
.join("\n");
|
|
776
|
+
return `${pad}<${tag}${attrs}>\n${children}\n${pad}</${tag}>`;
|
|
777
|
+
}
|
|
778
|
+
if (node instanceof VDocumentFragment) {
|
|
779
|
+
return node.childNodes
|
|
780
|
+
.map((c) => prettyPrint(c, depth, indent))
|
|
781
|
+
.filter(Boolean)
|
|
782
|
+
.join("\n");
|
|
783
|
+
}
|
|
784
|
+
if (node instanceof VDocument) {
|
|
785
|
+
const children = node.childNodes
|
|
786
|
+
.map((c) => prettyPrint(c, depth, indent))
|
|
787
|
+
.filter(Boolean)
|
|
788
|
+
.join("\n");
|
|
789
|
+
return `<!DOCTYPE html>\n${children}`;
|
|
790
|
+
}
|
|
791
|
+
return "";
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Convert any virtual DOM node to an HTML string.
|
|
795
|
+
*
|
|
796
|
+
* @param node The node to serialise.
|
|
797
|
+
* @param indent Indentation per level (default: `" "` — two spaces).
|
|
798
|
+
* Pass `""` for compact / non-indented output.
|
|
799
|
+
*
|
|
800
|
+
* - `VDocument` → `<!DOCTYPE html>\n<html>…</html>`
|
|
801
|
+
* - `VElement` → `<tag attrs>\n …children…\n</tag>`
|
|
802
|
+
* - `VText` → escaped text content
|
|
803
|
+
* - `VComment` → `<!--…-->`
|
|
804
|
+
* - `VDocumentFragment` → concatenated children
|
|
805
|
+
*/
|
|
806
|
+
function toHTML(node, indent = " ") {
|
|
807
|
+
if (indent === "")
|
|
808
|
+
return serializeNode(node);
|
|
809
|
+
return prettyPrint(node, 0, indent);
|
|
810
|
+
}
|
|
811
|
+
function escapeHtml(s) {
|
|
812
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
813
|
+
}
|
|
814
|
+
function escapeAttr(s) {
|
|
815
|
+
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
816
|
+
}
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// Very small CSS-selector matcher (covers the common cases)
|
|
819
|
+
// Supports: tag, .class, #id, [attr], [attr=value], and combos thereof
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
function makeSimpleMatcher(selector) {
|
|
822
|
+
// Split on commas for ,‑separated selectors
|
|
823
|
+
const parts = selector.split(",").map((s) => s.trim());
|
|
824
|
+
const matchers = parts.map(parseSingleSelector);
|
|
825
|
+
return (el) => matchers.some((m) => m(el));
|
|
826
|
+
}
|
|
827
|
+
function parseSingleSelector(sel) {
|
|
828
|
+
const checks = [];
|
|
829
|
+
// Tag name (must be at the start, before any # . or [)
|
|
830
|
+
const tagMatch = sel.match(/^([a-zA-Z][a-zA-Z0-9-]*)/);
|
|
831
|
+
if (tagMatch) {
|
|
832
|
+
const tag = tagMatch[1].toUpperCase();
|
|
833
|
+
checks.push((el) => el.tagName === tag);
|
|
834
|
+
}
|
|
835
|
+
// #id
|
|
836
|
+
for (const m of sel.matchAll(/#([a-zA-Z0-9_-]+)/g)) {
|
|
837
|
+
const id = m[1];
|
|
838
|
+
checks.push((el) => el.getAttribute("id") === id);
|
|
839
|
+
}
|
|
840
|
+
// .class
|
|
841
|
+
for (const m of sel.matchAll(/\.([a-zA-Z0-9_-]+)/g)) {
|
|
842
|
+
const cls = m[1];
|
|
843
|
+
checks.push((el) => el.classList.contains(cls));
|
|
844
|
+
}
|
|
845
|
+
// [attr] and [attr=value] and [attr="value"]
|
|
846
|
+
for (const m of sel.matchAll(/\[([a-zA-Z0-9_-]+)(?:=["']?([^"'\]]*)["']?)?\]/g)) {
|
|
847
|
+
const attr = m[1];
|
|
848
|
+
const val = m[2];
|
|
849
|
+
if (val !== undefined) {
|
|
850
|
+
checks.push((el) => el.getAttribute(attr) === val);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
checks.push((el) => el.hasAttribute(attr));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (checks.length === 0) {
|
|
857
|
+
return () => false;
|
|
858
|
+
}
|
|
859
|
+
return (el) => checks.every((fn) => fn(el));
|
|
860
|
+
}
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
// Utility
|
|
863
|
+
// ---------------------------------------------------------------------------
|
|
864
|
+
function camelCase(s) {
|
|
865
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
866
|
+
}
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
// Convenience factory (drop-in for tests)
|
|
869
|
+
// ---------------------------------------------------------------------------
|
|
870
|
+
/** Create a fresh virtual document, ready to use like `window.document`. */
|
|
871
|
+
function createDocument() {
|
|
872
|
+
return new VDocument();
|
|
873
|
+
}
|
|
874
|
+
//# sourceMappingURL=velement.js.map
|