@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.61",
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",
@@ -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 = new Element(this.ownerDocument, this.localName, this.__ns);
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 = new Element(this, tag, buf.ns(idx));
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 = new Element(this, raw.name, raw.namespace || '');
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 new Element(this, local, short);
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
+ ];
@@ -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: Element,
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.