@miaskiewicz/turbo-dom 0.1.9 → 0.1.11
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.11",
|
|
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
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,9 +27,12 @@ export function installGlobals(target, { html = DEFAULT_HTML, url } = {}) {
|
|
|
27
27
|
|
|
28
28
|
// window self-references
|
|
29
29
|
for (const k of SELF_KEYS) {
|
|
30
|
+
// window/self/parent/top resolve to the GLOBAL itself (target), like a real
|
|
31
|
+
// browser where window === globalThis. This is what makes vi.stubGlobal('x')
|
|
32
|
+
// visible via window.x — they're the same object/property, not two bindings.
|
|
30
33
|
define(k, {
|
|
31
34
|
configurable: true,
|
|
32
|
-
get: () =>
|
|
35
|
+
get: () => target,
|
|
33
36
|
set(v) { Object.defineProperty(target, k, { configurable: true, writable: true, value: v }); },
|
|
34
37
|
});
|
|
35
38
|
}
|
|
@@ -46,6 +49,10 @@ export function installGlobals(target, { html = DEFAULT_HTML, url } = {}) {
|
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
// window === globalThis (target): keep document.defaultView consistent so
|
|
53
|
+
// `window === document.defaultView` holds and stubs on the global are seen.
|
|
54
|
+
try { env.document.defaultView = target; } catch { /* getter-only in some setups */ }
|
|
55
|
+
|
|
49
56
|
// handy escape hatch
|
|
50
57
|
define('__turboDom', { configurable: true, writable: true, value: env });
|
|
51
58
|
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -380,7 +380,16 @@ export class Element extends Node {
|
|
|
380
380
|
const t = this.localName;
|
|
381
381
|
if (t === 'select') { for (const o of this.getElementsByTagName('option')) o.selected = (o.value === String(x)); return; }
|
|
382
382
|
if (t === 'option') { this.setAttribute('value', x); return; }
|
|
383
|
-
|
|
383
|
+
// typed <input> sanitizes per the value-sanitization algorithm: invalid
|
|
384
|
+
// values for date/time/number/etc are rejected (value left unchanged), so
|
|
385
|
+
// user-event typing them char-by-char doesn't emit bogus partial values.
|
|
386
|
+
if (t === 'input') {
|
|
387
|
+
const sanitized = sanitizeInputValue((this.getAttribute('type') || 'text').toLowerCase(), String(x));
|
|
388
|
+
if (sanitized === null) return; // invalid → reject, keep current value
|
|
389
|
+
this.__value = sanitized;
|
|
390
|
+
} else {
|
|
391
|
+
this.__value = String(x);
|
|
392
|
+
}
|
|
384
393
|
if (this.__selStart != null) { this.__selStart = Math.min(this.__selStart, this.__value.length); this.__selEnd = Math.min(this.__selEnd, this.__value.length); }
|
|
385
394
|
}
|
|
386
395
|
get defaultValue() { return this.getAttribute('value') ?? ''; }
|
|
@@ -425,6 +434,16 @@ export class Element extends Node {
|
|
|
425
434
|
set placeholder(x) { this.setAttribute('placeholder', x); }
|
|
426
435
|
get href() { return this.getAttribute('href') ?? ''; }
|
|
427
436
|
set href(x) { this.setAttribute('href', x); }
|
|
437
|
+
get download() { return this.getAttribute('download') ?? ''; }
|
|
438
|
+
set download(x) { this.setAttribute('download', x); }
|
|
439
|
+
get rel() { return this.getAttribute('rel') ?? ''; }
|
|
440
|
+
set rel(x) { this.setAttribute('rel', x); }
|
|
441
|
+
get referrerPolicy() { return this.getAttribute('referrerpolicy') ?? ''; }
|
|
442
|
+
set referrerPolicy(x) { this.setAttribute('referrerpolicy', x); }
|
|
443
|
+
get src() { return this.getAttribute('src') ?? ''; }
|
|
444
|
+
set src(x) { this.setAttribute('src', x); }
|
|
445
|
+
get alt() { return this.getAttribute('alt') ?? ''; }
|
|
446
|
+
set alt(x) { this.setAttribute('alt', x); }
|
|
428
447
|
|
|
429
448
|
// option
|
|
430
449
|
get selected() { return this.__selected !== undefined ? this.__selected : this.hasAttribute('selected'); }
|
|
@@ -860,6 +879,24 @@ function makeStyle(el) {
|
|
|
860
879
|
}
|
|
861
880
|
const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
862
881
|
|
|
882
|
+
// WHATWG value-sanitization per input type. Returns the accepted value, or null
|
|
883
|
+
// if invalid (caller leaves the current value unchanged — like a real browser,
|
|
884
|
+
// which rejects partial/garbage input rather than storing it).
|
|
885
|
+
function sanitizeInputValue(type, v) {
|
|
886
|
+
if (v === '') return '';
|
|
887
|
+
switch (type) {
|
|
888
|
+
case 'date': return /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : null;
|
|
889
|
+
case 'month': return /^\d{4}-\d{2}$/.test(v) ? v : null;
|
|
890
|
+
case 'week': return /^\d{4}-W\d{2}$/.test(v) ? v : null;
|
|
891
|
+
case 'time': return /^\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?$/.test(v) ? v : null;
|
|
892
|
+
case 'datetime-local': return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/.test(v) ? v : null;
|
|
893
|
+
case 'number': return /^-?\d*\.?\d+(e[-+]?\d+)?$/i.test(v) ? v : null;
|
|
894
|
+
case 'range': return /^-?\d*\.?\d+$/.test(v) ? v : '50';
|
|
895
|
+
case 'color': return /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : null;
|
|
896
|
+
default: return v; // text, email, password, search, url, tel, hidden, etc.
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
863
900
|
// element.dataset — camelCase <-> data-* attribute mapping.
|
|
864
901
|
const dataAttr = (key) => 'data-' + key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
865
902
|
const dataKey = (attr) => attr.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -135,11 +135,71 @@ function matchPseudo(el, p) {
|
|
|
135
135
|
return el.childNodes.length === 0;
|
|
136
136
|
case 'root':
|
|
137
137
|
return el.parentNode == null || el.parentNode.nodeType === 9;
|
|
138
|
+
// form-state pseudo-classes — read the live PROPERTY (what React sets),
|
|
139
|
+
// not just the HTML attribute.
|
|
140
|
+
case 'checked':
|
|
141
|
+
return el.localName === 'option' ? !!el.selected : !!el.checked;
|
|
142
|
+
case 'disabled':
|
|
143
|
+
return !!el.disabled || el.hasAttribute('disabled');
|
|
144
|
+
case 'enabled':
|
|
145
|
+
return /^(input|button|select|textarea|optgroup|option|fieldset)$/.test(el.localName) && !(el.disabled || el.hasAttribute('disabled'));
|
|
146
|
+
case 'required':
|
|
147
|
+
return !!el.required || el.hasAttribute('required');
|
|
148
|
+
case 'optional':
|
|
149
|
+
return /^(input|select|textarea)$/.test(el.localName) && !(el.required || el.hasAttribute('required'));
|
|
150
|
+
case 'read-only':
|
|
151
|
+
return el.hasAttribute('readonly') || !!el.readOnly;
|
|
152
|
+
case 'read-write':
|
|
153
|
+
return !(el.hasAttribute('readonly') || el.readOnly);
|
|
154
|
+
case 'selected':
|
|
155
|
+
return !!el.selected;
|
|
156
|
+
// positional pseudo-classes (1-based, support An+B / odd / even)
|
|
157
|
+
case 'nth-child':
|
|
158
|
+
return nthMatch(p.arg, siblingIndex(el) + 1);
|
|
159
|
+
case 'nth-last-child':
|
|
160
|
+
return nthMatch(p.arg, siblingCount(el) - siblingIndex(el));
|
|
161
|
+
case 'nth-of-type':
|
|
162
|
+
return nthMatch(p.arg, typeIndex(el) + 1);
|
|
163
|
+
case 'nth-last-of-type':
|
|
164
|
+
return nthMatch(p.arg, typeCount(el) - typeIndex(el));
|
|
165
|
+
case 'first-of-type':
|
|
166
|
+
return typeIndex(el) === 0;
|
|
167
|
+
case 'last-of-type':
|
|
168
|
+
return typeIndex(el) === typeCount(el) - 1;
|
|
169
|
+
case 'only-of-type':
|
|
170
|
+
return typeCount(el) === 1;
|
|
138
171
|
default:
|
|
139
172
|
return false; // unknown pseudo: never matches (honest, not a silent true)
|
|
140
173
|
}
|
|
141
174
|
}
|
|
142
175
|
|
|
176
|
+
function elementSiblings(el) {
|
|
177
|
+
const p = el.parentNode;
|
|
178
|
+
if (!p) return [el];
|
|
179
|
+
const kids = typeof p.__children === 'function' ? p.__children() : Array.from(p.childNodes || []);
|
|
180
|
+
return kids.filter((n) => n.nodeType === 1);
|
|
181
|
+
}
|
|
182
|
+
function siblingIndex(el) { return elementSiblings(el).indexOf(el); }
|
|
183
|
+
function siblingCount(el) { return elementSiblings(el).length; }
|
|
184
|
+
function typeIndex(el) { return elementSiblings(el).filter((n) => n.localName === el.localName).indexOf(el); }
|
|
185
|
+
function typeCount(el) { return elementSiblings(el).filter((n) => n.localName === el.localName).length; }
|
|
186
|
+
|
|
187
|
+
// match An+B / odd / even / integer against a 1-based index
|
|
188
|
+
function nthMatch(arg, index) {
|
|
189
|
+
const a0 = String(arg || '').trim().toLowerCase();
|
|
190
|
+
if (a0 === 'odd') return index % 2 === 1;
|
|
191
|
+
if (a0 === 'even') return index % 2 === 0;
|
|
192
|
+
const m = /^([+-]?\d*)n\s*([+-]\s*\d+)?$/.exec(a0.replace(/\s+/g, ''));
|
|
193
|
+
if (m) {
|
|
194
|
+
let a = m[1] === '' || m[1] === '+' ? 1 : m[1] === '-' ? -1 : parseInt(m[1], 10);
|
|
195
|
+
const b = m[2] ? parseInt(m[2].replace(/\s/g, ''), 10) : 0;
|
|
196
|
+
if (a === 0) return index === b;
|
|
197
|
+
return (index - b) % a === 0 && (index - b) / a >= 0;
|
|
198
|
+
}
|
|
199
|
+
const n = parseInt(a0, 10);
|
|
200
|
+
return !Number.isNaN(n) && index === n;
|
|
201
|
+
}
|
|
202
|
+
|
|
143
203
|
function previousElement(el) {
|
|
144
204
|
let n = el.previousSibling;
|
|
145
205
|
while (n && n.nodeType !== 1) n = n.previousSibling;
|
|
Binary file
|