@miaskiewicz/turbo-dom 0.1.61 → 0.1.62
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.62",
|
|
4
4
|
"description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { liveNodeList, liveHTMLCollection } from './collections.mjs';
|
|
|
15
15
|
import { matchesSelector, querySelector as qsel, querySelectorAll as qselAll } from './selectors.mjs';
|
|
16
16
|
import { serializeInner, serializeOuter } from './html-serialize.mjs';
|
|
17
17
|
import { CSSStyleSheet, styleSheetList } from './cssom.mjs';
|
|
18
|
+
import { SVGAnimatedString, SVGAnimatedLength, SVGAnimatedRect, SVG_LENGTH_ATTRS } from './svg.mjs';
|
|
18
19
|
|
|
19
20
|
// Per-node query-result cache keyed by (selector, document version). querySelectorAll
|
|
20
21
|
// returns a STATIC list per spec, so caching is safe until the next mutation bumps
|
|
@@ -724,7 +725,7 @@ export class Element extends Node {
|
|
|
724
725
|
}
|
|
725
726
|
|
|
726
727
|
cloneNode(deep = false) {
|
|
727
|
-
const el =
|
|
728
|
+
const el = newElement(this.ownerDocument, this.localName, this.__ns);
|
|
728
729
|
el.__attrs = (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => ({ ...a }));
|
|
729
730
|
if (deep) {
|
|
730
731
|
// build the cloned child array directly — the clone is detached, so per-child
|
|
@@ -935,6 +936,46 @@ export class Element extends Node {
|
|
|
935
936
|
}
|
|
936
937
|
|
|
937
938
|
|
|
939
|
+
// ------------------------------------------------------------- SVGElement ----
|
|
940
|
+
// SVG elements expose their geometry/identity attributes as SVGAnimated{String,Length,
|
|
941
|
+
// Rect} objects (el.className.baseVal, rect.width.baseVal.value, svg.viewBox.baseVal),
|
|
942
|
+
// not the plain strings/numbers HTML uses. A real subclass keeps these off every HTML
|
|
943
|
+
// element (zero regression, zero hot-path cost — the wrappers are built on access only).
|
|
944
|
+
// Instantiated by newElement() whenever the namespace is "svg".
|
|
945
|
+
export class SVGElement extends Element {
|
|
946
|
+
// className is a readonly SVGAnimatedString in the spec; we still allow assigning a
|
|
947
|
+
// string (common habit) by writing the class attribute, but reads return the wrapper.
|
|
948
|
+
get className() { return new SVGAnimatedString(this, 'class'); }
|
|
949
|
+
set className(v) { this.setAttribute('class', String(v)); }
|
|
950
|
+
|
|
951
|
+
get viewBox() { return new SVGAnimatedRect(this, 'viewBox'); }
|
|
952
|
+
|
|
953
|
+
// Honest geometry stubs (consistent with stubs.mjs — no layout engine, so zeros).
|
|
954
|
+
getBBox() { return { x: 0, y: 0, width: 0, height: 0 }; }
|
|
955
|
+
getCTM() { return null; }
|
|
956
|
+
getScreenCTM() { return null; }
|
|
957
|
+
get ownerSVGElement() {
|
|
958
|
+
let p = this.parentNode;
|
|
959
|
+
while (p && p.nodeType === ELEMENT_NODE) { if (p.localName === 'svg') return p; p = p.parentNode; }
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// Length-valued geometry attributes → SVGAnimatedLength (built lazily per read).
|
|
964
|
+
for (const attr of SVG_LENGTH_ATTRS) {
|
|
965
|
+
Object.defineProperty(SVGElement.prototype, attr, {
|
|
966
|
+
configurable: true,
|
|
967
|
+
get() { return new SVGAnimatedLength(this, attr); },
|
|
968
|
+
set(v) { this.setAttribute(attr, v != null && v.baseVal ? String(v.baseVal.value) : String(v)); },
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Element factory: SVG namespace → SVGElement, everything else → Element. One place so
|
|
973
|
+
// every construction site (buffer inflation, innerHTML, cloneNode, createElementNS) agrees.
|
|
974
|
+
function newElement(doc, tag, ns) {
|
|
975
|
+
return ns === 'svg' ? new SVGElement(doc, tag, ns) : new Element(doc, tag, ns);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
938
979
|
// ---------------------------------------------------- DocumentFragment ----
|
|
939
980
|
export class DocumentFragment extends Node {
|
|
940
981
|
get nodeType() { return DOCUMENT_FRAGMENT_NODE; }
|
|
@@ -1473,7 +1514,7 @@ export class Document extends Node {
|
|
|
1473
1514
|
switch (nt) {
|
|
1474
1515
|
case ELEMENT_NODE: {
|
|
1475
1516
|
const tag = buf.tagName(idx); // read once (was read twice — ctor + template check)
|
|
1476
|
-
node =
|
|
1517
|
+
node = newElement(this, tag, buf.ns(idx));
|
|
1477
1518
|
node.__idx = idx;
|
|
1478
1519
|
node.__attrIdx = idx; // attrs lazy (constructor left __attrs undefined)
|
|
1479
1520
|
// template content fragment: a child node typed 11 named "content"
|
|
@@ -1505,7 +1546,7 @@ export class Document extends Node {
|
|
|
1505
1546
|
let node;
|
|
1506
1547
|
switch (raw.nodeType) {
|
|
1507
1548
|
case ELEMENT_NODE:
|
|
1508
|
-
node =
|
|
1549
|
+
node = newElement(this, raw.name, raw.namespace || '');
|
|
1509
1550
|
node.__attrs = raw.attrs.map((a) => ({ name: a.name, value: a.value, prefix: a.prefix || '' }));
|
|
1510
1551
|
if (raw.name === 'template') {
|
|
1511
1552
|
const contentRaw = raw.children.find((c) => c.nodeType === DOCUMENT_FRAGMENT_NODE && c.name === 'content');
|
|
@@ -1567,7 +1608,7 @@ export class Document extends Node {
|
|
|
1567
1608
|
createElementNS(ns, qualified) {
|
|
1568
1609
|
const short = ns === SVG_NS ? 'svg' : ns === MATHML_NS ? 'math' : '';
|
|
1569
1610
|
const local = qualified.includes(':') ? qualified.split(':')[1] : qualified;
|
|
1570
|
-
return
|
|
1611
|
+
return newElement(this, local, short);
|
|
1571
1612
|
}
|
|
1572
1613
|
createTextNode(data) { return new Text(this, String(data)); }
|
|
1573
1614
|
createComment(data) { return new Comment(this, String(data)); }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Minimal SVG DOM-property wrappers. SVG IDL attributes are not plain strings/numbers
|
|
2
|
+
// like HTML reflected attributes — they're SVGAnimated{String,Length,Rect} objects with
|
|
3
|
+
// .baseVal / .animVal. Component code (and the libraries that render SVG icons) reads
|
|
4
|
+
// `el.className.baseVal`, `rect.width.baseVal.value`, `svg.viewBox.baseVal`, etc.; without
|
|
5
|
+
// these wrappers those reads throw "Cannot read properties of undefined (reading 'baseVal')".
|
|
6
|
+
//
|
|
7
|
+
// HONEST + LIVE: every wrapper reads/writes the underlying attribute on demand (no cached
|
|
8
|
+
// snapshot), so it always reflects the current attribute and never invents a value. There's
|
|
9
|
+
// no animation engine — animVal === baseVal. Only attached to real SVGElement instances
|
|
10
|
+
// (see dom.mjs), so HTML elements are completely unaffected.
|
|
11
|
+
|
|
12
|
+
// SVGLength — the unit-bearing value behind an SVGAnimatedLength. valueOf() returns the
|
|
13
|
+
// number so `rect.width.baseVal` used in arithmetic/comparisons coerces correctly, while
|
|
14
|
+
// `rect.width.baseVal.value` reads the same number explicitly.
|
|
15
|
+
export class SVGLength {
|
|
16
|
+
constructor(host, attr) { this.__host = host; this.__attr = attr; }
|
|
17
|
+
get value() { const v = parseFloat(this.__host.getAttribute(this.__attr)); return Number.isNaN(v) ? 0 : v; }
|
|
18
|
+
set value(v) { this.__host.setAttribute(this.__attr, String(v)); }
|
|
19
|
+
get valueInSpecifiedUnits() { return this.value; }
|
|
20
|
+
get unitType() { return 1; } // SVG_LENGTHTYPE_NUMBER
|
|
21
|
+
get valueAsString() { return this.__host.getAttribute(this.__attr) || '0'; }
|
|
22
|
+
set valueAsString(s) { this.__host.setAttribute(this.__attr, String(s)); }
|
|
23
|
+
valueOf() { return this.value; }
|
|
24
|
+
toString() { return this.valueAsString; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SVGAnimatedLength {
|
|
28
|
+
constructor(host, attr) {
|
|
29
|
+
const base = new SVGLength(host, attr);
|
|
30
|
+
this.baseVal = base;
|
|
31
|
+
this.animVal = base; // no animation engine — animated value tracks the base value
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SVGAnimatedString {
|
|
36
|
+
constructor(host, attr) { this.__host = host; this.__attr = attr; }
|
|
37
|
+
get baseVal() { return this.__host.getAttribute(this.__attr) || ''; }
|
|
38
|
+
set baseVal(v) { this.__host.setAttribute(this.__attr, String(v)); }
|
|
39
|
+
get animVal() { return this.baseVal; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// SVGRect / SVGAnimatedRect — backs `viewBox`. Parses the four-number list lazily.
|
|
43
|
+
export class SVGRect {
|
|
44
|
+
constructor(host, attr) { this.__host = host; this.__attr = attr; }
|
|
45
|
+
#nums() {
|
|
46
|
+
const raw = this.__host.getAttribute(this.__attr) || '';
|
|
47
|
+
const parts = raw.split(/[\s,]+/).map(parseFloat);
|
|
48
|
+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0, parts[3] || 0];
|
|
49
|
+
}
|
|
50
|
+
get x() { return this.#nums()[0]; }
|
|
51
|
+
get y() { return this.#nums()[1]; }
|
|
52
|
+
get width() { return this.#nums()[2]; }
|
|
53
|
+
get height() { return this.#nums()[3]; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class SVGAnimatedRect {
|
|
57
|
+
constructor(host, attr) {
|
|
58
|
+
const base = new SVGRect(host, attr);
|
|
59
|
+
this.baseVal = base;
|
|
60
|
+
this.animVal = base;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Attributes that surface as SVGAnimatedLength on SVG elements. Covers the common
|
|
65
|
+
// geometry set (shapes, gradients, markers); any not listed still works via getAttribute.
|
|
66
|
+
export const SVG_LENGTH_ATTRS = [
|
|
67
|
+
'width', 'height', 'x', 'y', 'cx', 'cy', 'r', 'rx', 'ry',
|
|
68
|
+
'x1', 'y1', 'x2', 'y2', 'dx', 'dy', 'fx', 'fy', 'refX', 'refY',
|
|
69
|
+
'markerWidth', 'markerHeight', 'startOffset', 'textLength',
|
|
70
|
+
];
|
package/src/runtime/window.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { makeGetComputedStyle } from './cascade.mjs';
|
|
14
14
|
import { CSSStyleSheet } from './cssom.mjs';
|
|
15
15
|
import {
|
|
16
|
-
Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
|
|
16
|
+
Node, Element, SVGElement, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
|
|
17
17
|
MutationObserver, DOMParser, XMLSerializer, ShadowRoot,
|
|
18
18
|
} from './dom.mjs';
|
|
19
19
|
import {
|
|
@@ -282,7 +282,7 @@ const STATIC_BASE = {
|
|
|
282
282
|
DataTransfer,
|
|
283
283
|
// every element is a plain Element → `el instanceof HTMLElement` is true.
|
|
284
284
|
// Tag-specific interfaces match by localName via Symbol.hasInstance.
|
|
285
|
-
HTMLElement: Element, SVGElement
|
|
285
|
+
HTMLElement: Element, SVGElement,
|
|
286
286
|
// SVG interface globals. Libs (and test setup mocks) do
|
|
287
287
|
// `Object.defineProperty(SVGSVGElement.prototype, 'viewBox', …)` and
|
|
288
288
|
// `el instanceof SVGSVGElement`; an undefined global is a hard ReferenceError.
|
|
Binary file
|