@miaskiewicz/turbo-dom 0.1.57 → 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/README.md CHANGED
@@ -85,6 +85,37 @@ parseBuffer('<div id=a>...</div>'); // compact SoA typed-array bu
85
85
  parseFragment('<rect/>', 'svg path'); // fragment in a context element
86
86
  ```
87
87
 
88
+ ### Choosing the parser backend (native vs WASM)
89
+
90
+ The runtime resolves a parser lazily on first parse: native N-API addon first, WASM
91
+ fallback. Force it per environment or globally:
92
+
93
+ ```js
94
+ import { createEnvironment, setParserMode } from '@miaskiewicz/turbo-dom/runtime';
95
+
96
+ createEnvironment(html, { parser: 'wasm' }); // 'wasm' | 'native' | 'auto' (default)
97
+ setParserMode('wasm'); // process-global; also TURBO_DOM_PARSER=wasm
98
+ ```
99
+
100
+ ### Embedding in a non-Node runtime (bare V8, no Node builtins)
101
+
102
+ The native addon and the `--target nodejs` WASM build both need Node. For a fully
103
+ node-free host, instantiate the `--target web` WASM yourself and inject the binding —
104
+ the runtime then never touches `node:module`/`fs`:
105
+
106
+ ```js
107
+ import init, { initSync, parse, parseBuffer, parseFragment } from '@miaskiewicz/turbo-dom/parser-wasm';
108
+ import { setParser, createEnvironment } from '@miaskiewicz/turbo-dom/runtime';
109
+
110
+ initSync({ module: wasmBytes }); // you supply the bytes (sync, no fs)
111
+ setParser({ parse, parseBuffer, parseFragment });
112
+ createEnvironment('<div id=app/>'); // now runs with zero Node deps
113
+ ```
114
+
115
+ `setParser` also reads `globalThis.__TURBO_DOM_PARSER__` if you prefer injection by
116
+ global. Use `installGlobals(globalThis, { html })` (from `@miaskiewicz/turbo-dom/install`)
117
+ to set up `document`/`window` on any global object.
118
+
88
119
  ## Compatibility
89
120
 
90
121
  | | turbo-dom | happy-dom | jsdom |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.1.57",
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",
@@ -18,6 +18,16 @@
18
18
  "./environment/vitest": {
19
19
  "import": "./src/environment/vitest.mjs"
20
20
  },
21
+ "./environment/install": {
22
+ "import": "./src/environment/install.mjs"
23
+ },
24
+ "./install": {
25
+ "import": "./src/environment/install.mjs"
26
+ },
27
+ "./parser-wasm": {
28
+ "types": "./pkg-web/turbo_dom_parser.d.ts",
29
+ "import": "./pkg-web/turbo_dom_parser.js"
30
+ },
21
31
  "./jest": {
22
32
  "require": "./src/environment/jest.cjs"
23
33
  },
@@ -27,6 +37,14 @@
27
37
  "index.js",
28
38
  "index.d.ts",
29
39
  "*.node",
40
+ "pkg/turbo_dom_parser.js",
41
+ "pkg/turbo_dom_parser.d.ts",
42
+ "pkg/turbo_dom_parser_bg.wasm",
43
+ "pkg/turbo_dom_parser_bg.wasm.d.ts",
44
+ "pkg-web/turbo_dom_parser.js",
45
+ "pkg-web/turbo_dom_parser.d.ts",
46
+ "pkg-web/turbo_dom_parser_bg.wasm",
47
+ "pkg-web/turbo_dom_parser_bg.wasm.d.ts",
30
48
  "src/**/*.mjs",
31
49
  "src/**/*.cjs",
32
50
  "src/**/*.d.ts",
@@ -51,12 +69,13 @@
51
69
  }
52
70
  },
53
71
  "scripts": {
54
- "build": "napi build --platform --release",
72
+ "build": "napi build --platform --release && npm run build:wasm:pkg && npm run build:wasm:web",
55
73
  "build:debug": "napi build --platform",
56
74
  "build:wasm": "cargo build --release --no-default-features --features wasm-bind --target wasm32-unknown-unknown",
57
75
  "build:wasm:pkg": "wasm-pack build --target nodejs --out-dir pkg --no-default-features --features wasm-bind",
76
+ "build:wasm:web": "wasm-pack build --target web --out-dir pkg-web --no-default-features --features wasm-bind",
58
77
  "test": "node --test 'test/*.mjs'",
59
- "test:cov": "node --test --experimental-test-coverage --test-coverage-include='src/runtime/**' --test-coverage-lines=99 --test-coverage-functions=87 --test-coverage-branches=87 'test/*.mjs'",
78
+ "test:cov": "node --test --experimental-test-coverage --test-coverage-include='src/runtime/**' --test-coverage-lines=99 --test-coverage-functions=92 --test-coverage-branches=88 'test/*.mjs'",
60
79
  "test:rust": "cargo test --lib",
61
80
  "test:all": "npm run build && npm run test:rust && npm test",
62
81
  "prepare": "git config core.hooksPath .githooks || true",
@@ -67,7 +86,7 @@
67
86
  "bench:suite": "node bench/suite.mjs",
68
87
  "bench:wasm": "node bench/wasm.mjs",
69
88
  "bench:all": "node bench/parse.mjs && node bench/construct.mjs && node bench/suite.mjs && node bench/query.mjs && node bench/wasm.mjs",
70
- "prepublishOnly": "napi build --platform --release",
89
+ "prepublishOnly": "napi build --platform --release && npm run build:wasm:pkg && npm run build:wasm:web",
71
90
  "test:vitest": "vitest run -c test-vitest/vitest.config.mjs",
72
91
  "test:jest": "jest -c test-jest/jest.config.cjs",
73
92
  "bench:query": "node bench/query.mjs",
@@ -0,0 +1,11 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Same contract as the native addon, marshaled via serde → JS object.
6
+ */
7
+ export function parse(html: string): any;
8
+
9
+ export function parseBuffer(html: string): any;
10
+
11
+ export function parseFragment(html: string, context?: string | null): any;
@@ -0,0 +1,197 @@
1
+ /* @ts-self-types="./turbo_dom_parser.d.ts" */
2
+
3
+ /**
4
+ * Same contract as the native addon, marshaled via serde → JS object.
5
+ * @param {string} html
6
+ * @returns {any}
7
+ */
8
+ function parse(html) {
9
+ const ptr0 = passStringToWasm0(html, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
10
+ const len0 = WASM_VECTOR_LEN;
11
+ const ret = wasm.parse(ptr0, len0);
12
+ if (ret[2]) {
13
+ throw takeFromExternrefTable0(ret[1]);
14
+ }
15
+ return takeFromExternrefTable0(ret[0]);
16
+ }
17
+ exports.parse = parse;
18
+
19
+ /**
20
+ * @param {string} html
21
+ * @returns {any}
22
+ */
23
+ function parseBuffer(html) {
24
+ const ptr0 = passStringToWasm0(html, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
25
+ const len0 = WASM_VECTOR_LEN;
26
+ const ret = wasm.parseBuffer(ptr0, len0);
27
+ if (ret[2]) {
28
+ throw takeFromExternrefTable0(ret[1]);
29
+ }
30
+ return takeFromExternrefTable0(ret[0]);
31
+ }
32
+ exports.parseBuffer = parseBuffer;
33
+
34
+ /**
35
+ * @param {string} html
36
+ * @param {string | null} [context]
37
+ * @returns {any}
38
+ */
39
+ function parseFragment(html, context) {
40
+ const ptr0 = passStringToWasm0(html, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
41
+ const len0 = WASM_VECTOR_LEN;
42
+ var ptr1 = isLikeNone(context) ? 0 : passStringToWasm0(context, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
43
+ var len1 = WASM_VECTOR_LEN;
44
+ const ret = wasm.parseFragment(ptr0, len0, ptr1, len1);
45
+ if (ret[2]) {
46
+ throw takeFromExternrefTable0(ret[1]);
47
+ }
48
+ return takeFromExternrefTable0(ret[0]);
49
+ }
50
+ exports.parseFragment = parseFragment;
51
+ function __wbg_get_imports() {
52
+ const import0 = {
53
+ __proto__: null,
54
+ __wbg_String_8564e559799eccda: function(arg0, arg1) {
55
+ const ret = String(arg1);
56
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
57
+ const len1 = WASM_VECTOR_LEN;
58
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
59
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
60
+ },
61
+ __wbg___wbindgen_throw_1506f2235d1bdba0: function(arg0, arg1) {
62
+ throw new Error(getStringFromWasm0(arg0, arg1));
63
+ },
64
+ __wbg_new_ce1ab61c1c2b300d: function() {
65
+ const ret = new Object();
66
+ return ret;
67
+ },
68
+ __wbg_new_d90091b82fdf5b91: function() {
69
+ const ret = new Array();
70
+ return ret;
71
+ },
72
+ __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
73
+ arg0[arg1] = arg2;
74
+ },
75
+ __wbg_set_dca99999bba88a9a: function(arg0, arg1, arg2) {
76
+ arg0[arg1 >>> 0] = arg2;
77
+ },
78
+ __wbindgen_cast_0000000000000001: function(arg0) {
79
+ // Cast intrinsic for `F64 -> Externref`.
80
+ const ret = arg0;
81
+ return ret;
82
+ },
83
+ __wbindgen_cast_0000000000000002: function(arg0, arg1) {
84
+ // Cast intrinsic for `Ref(String) -> Externref`.
85
+ const ret = getStringFromWasm0(arg0, arg1);
86
+ return ret;
87
+ },
88
+ __wbindgen_init_externref_table: function() {
89
+ const table = wasm.__wbindgen_externrefs;
90
+ const offset = table.grow(4);
91
+ table.set(0, undefined);
92
+ table.set(offset + 0, undefined);
93
+ table.set(offset + 1, null);
94
+ table.set(offset + 2, true);
95
+ table.set(offset + 3, false);
96
+ },
97
+ };
98
+ return {
99
+ __proto__: null,
100
+ "./turbo_dom_parser_bg.js": import0,
101
+ };
102
+ }
103
+
104
+ let cachedDataViewMemory0 = null;
105
+ function getDataViewMemory0() {
106
+ if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
107
+ cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
108
+ }
109
+ return cachedDataViewMemory0;
110
+ }
111
+
112
+ function getStringFromWasm0(ptr, len) {
113
+ return decodeText(ptr >>> 0, len);
114
+ }
115
+
116
+ let cachedUint8ArrayMemory0 = null;
117
+ function getUint8ArrayMemory0() {
118
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
119
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
120
+ }
121
+ return cachedUint8ArrayMemory0;
122
+ }
123
+
124
+ function isLikeNone(x) {
125
+ return x === undefined || x === null;
126
+ }
127
+
128
+ function passStringToWasm0(arg, malloc, realloc) {
129
+ if (realloc === undefined) {
130
+ const buf = cachedTextEncoder.encode(arg);
131
+ const ptr = malloc(buf.length, 1) >>> 0;
132
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
133
+ WASM_VECTOR_LEN = buf.length;
134
+ return ptr;
135
+ }
136
+
137
+ let len = arg.length;
138
+ let ptr = malloc(len, 1) >>> 0;
139
+
140
+ const mem = getUint8ArrayMemory0();
141
+
142
+ let offset = 0;
143
+
144
+ for (; offset < len; offset++) {
145
+ const code = arg.charCodeAt(offset);
146
+ if (code > 0x7F) break;
147
+ mem[ptr + offset] = code;
148
+ }
149
+ if (offset !== len) {
150
+ if (offset !== 0) {
151
+ arg = arg.slice(offset);
152
+ }
153
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
154
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
155
+ const ret = cachedTextEncoder.encodeInto(arg, view);
156
+
157
+ offset += ret.written;
158
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
159
+ }
160
+
161
+ WASM_VECTOR_LEN = offset;
162
+ return ptr;
163
+ }
164
+
165
+ function takeFromExternrefTable0(idx) {
166
+ const value = wasm.__wbindgen_externrefs.get(idx);
167
+ wasm.__externref_table_dealloc(idx);
168
+ return value;
169
+ }
170
+
171
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
172
+ cachedTextDecoder.decode();
173
+ function decodeText(ptr, len) {
174
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
175
+ }
176
+
177
+ const cachedTextEncoder = new TextEncoder();
178
+
179
+ if (!('encodeInto' in cachedTextEncoder)) {
180
+ cachedTextEncoder.encodeInto = function (arg, view) {
181
+ const buf = cachedTextEncoder.encode(arg);
182
+ view.set(buf);
183
+ return {
184
+ read: arg.length,
185
+ written: buf.length
186
+ };
187
+ };
188
+ }
189
+
190
+ let WASM_VECTOR_LEN = 0;
191
+
192
+ const wasmPath = `${__dirname}/turbo_dom_parser_bg.wasm`;
193
+ const wasmBytes = require('fs').readFileSync(wasmPath);
194
+ const wasmModule = new WebAssembly.Module(wasmBytes);
195
+ let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
196
+ let wasm = wasmInstance.exports;
197
+ wasm.__wbindgen_start();
Binary file
@@ -0,0 +1,11 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const parse: (a: number, b: number) => [number, number, number];
5
+ export const parseBuffer: (a: number, b: number) => [number, number, number];
6
+ export const parseFragment: (a: number, b: number, c: number, d: number) => [number, number, number];
7
+ export const __wbindgen_malloc: (a: number, b: number) => number;
8
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
9
+ export const __wbindgen_externrefs: WebAssembly.Table;
10
+ export const __externref_table_dealloc: (a: number) => void;
11
+ export const __wbindgen_start: () => void;
@@ -0,0 +1,47 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Same contract as the native addon, marshaled via serde → JS object.
6
+ */
7
+ export function parse(html: string): any;
8
+
9
+ export function parseBuffer(html: string): any;
10
+
11
+ export function parseFragment(html: string, context?: string | null): any;
12
+
13
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
14
+
15
+ export interface InitOutput {
16
+ readonly memory: WebAssembly.Memory;
17
+ readonly parse: (a: number, b: number) => [number, number, number];
18
+ readonly parseBuffer: (a: number, b: number) => [number, number, number];
19
+ readonly parseFragment: (a: number, b: number, c: number, d: number) => [number, number, number];
20
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
21
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
22
+ readonly __wbindgen_externrefs: WebAssembly.Table;
23
+ readonly __externref_table_dealloc: (a: number) => void;
24
+ readonly __wbindgen_start: () => void;
25
+ }
26
+
27
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
28
+
29
+ /**
30
+ * Instantiates the given `module`, which can either be bytes or
31
+ * a precompiled `WebAssembly.Module`.
32
+ *
33
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
34
+ *
35
+ * @returns {InitOutput}
36
+ */
37
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
38
+
39
+ /**
40
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
41
+ * for everything else, calls `WebAssembly.instantiate` directly.
42
+ *
43
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
44
+ *
45
+ * @returns {Promise<InitOutput>}
46
+ */
47
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
@@ -0,0 +1,289 @@
1
+ /* @ts-self-types="./turbo_dom_parser.d.ts" */
2
+
3
+ /**
4
+ * Same contract as the native addon, marshaled via serde → JS object.
5
+ * @param {string} html
6
+ * @returns {any}
7
+ */
8
+ export function parse(html) {
9
+ const ptr0 = passStringToWasm0(html, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
10
+ const len0 = WASM_VECTOR_LEN;
11
+ const ret = wasm.parse(ptr0, len0);
12
+ if (ret[2]) {
13
+ throw takeFromExternrefTable0(ret[1]);
14
+ }
15
+ return takeFromExternrefTable0(ret[0]);
16
+ }
17
+
18
+ /**
19
+ * @param {string} html
20
+ * @returns {any}
21
+ */
22
+ export function parseBuffer(html) {
23
+ const ptr0 = passStringToWasm0(html, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
24
+ const len0 = WASM_VECTOR_LEN;
25
+ const ret = wasm.parseBuffer(ptr0, len0);
26
+ if (ret[2]) {
27
+ throw takeFromExternrefTable0(ret[1]);
28
+ }
29
+ return takeFromExternrefTable0(ret[0]);
30
+ }
31
+
32
+ /**
33
+ * @param {string} html
34
+ * @param {string | null} [context]
35
+ * @returns {any}
36
+ */
37
+ export function parseFragment(html, context) {
38
+ const ptr0 = passStringToWasm0(html, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
39
+ const len0 = WASM_VECTOR_LEN;
40
+ var ptr1 = isLikeNone(context) ? 0 : passStringToWasm0(context, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
41
+ var len1 = WASM_VECTOR_LEN;
42
+ const ret = wasm.parseFragment(ptr0, len0, ptr1, len1);
43
+ if (ret[2]) {
44
+ throw takeFromExternrefTable0(ret[1]);
45
+ }
46
+ return takeFromExternrefTable0(ret[0]);
47
+ }
48
+ function __wbg_get_imports() {
49
+ const import0 = {
50
+ __proto__: null,
51
+ __wbg_String_8564e559799eccda: function(arg0, arg1) {
52
+ const ret = String(arg1);
53
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
54
+ const len1 = WASM_VECTOR_LEN;
55
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
56
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
57
+ },
58
+ __wbg___wbindgen_throw_1506f2235d1bdba0: function(arg0, arg1) {
59
+ throw new Error(getStringFromWasm0(arg0, arg1));
60
+ },
61
+ __wbg_new_ce1ab61c1c2b300d: function() {
62
+ const ret = new Object();
63
+ return ret;
64
+ },
65
+ __wbg_new_d90091b82fdf5b91: function() {
66
+ const ret = new Array();
67
+ return ret;
68
+ },
69
+ __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
70
+ arg0[arg1] = arg2;
71
+ },
72
+ __wbg_set_dca99999bba88a9a: function(arg0, arg1, arg2) {
73
+ arg0[arg1 >>> 0] = arg2;
74
+ },
75
+ __wbindgen_cast_0000000000000001: function(arg0) {
76
+ // Cast intrinsic for `F64 -> Externref`.
77
+ const ret = arg0;
78
+ return ret;
79
+ },
80
+ __wbindgen_cast_0000000000000002: function(arg0, arg1) {
81
+ // Cast intrinsic for `Ref(String) -> Externref`.
82
+ const ret = getStringFromWasm0(arg0, arg1);
83
+ return ret;
84
+ },
85
+ __wbindgen_init_externref_table: function() {
86
+ const table = wasm.__wbindgen_externrefs;
87
+ const offset = table.grow(4);
88
+ table.set(0, undefined);
89
+ table.set(offset + 0, undefined);
90
+ table.set(offset + 1, null);
91
+ table.set(offset + 2, true);
92
+ table.set(offset + 3, false);
93
+ },
94
+ };
95
+ return {
96
+ __proto__: null,
97
+ "./turbo_dom_parser_bg.js": import0,
98
+ };
99
+ }
100
+
101
+ let cachedDataViewMemory0 = null;
102
+ function getDataViewMemory0() {
103
+ if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
104
+ cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
105
+ }
106
+ return cachedDataViewMemory0;
107
+ }
108
+
109
+ function getStringFromWasm0(ptr, len) {
110
+ return decodeText(ptr >>> 0, len);
111
+ }
112
+
113
+ let cachedUint8ArrayMemory0 = null;
114
+ function getUint8ArrayMemory0() {
115
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
116
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
117
+ }
118
+ return cachedUint8ArrayMemory0;
119
+ }
120
+
121
+ function isLikeNone(x) {
122
+ return x === undefined || x === null;
123
+ }
124
+
125
+ function passStringToWasm0(arg, malloc, realloc) {
126
+ if (realloc === undefined) {
127
+ const buf = cachedTextEncoder.encode(arg);
128
+ const ptr = malloc(buf.length, 1) >>> 0;
129
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
130
+ WASM_VECTOR_LEN = buf.length;
131
+ return ptr;
132
+ }
133
+
134
+ let len = arg.length;
135
+ let ptr = malloc(len, 1) >>> 0;
136
+
137
+ const mem = getUint8ArrayMemory0();
138
+
139
+ let offset = 0;
140
+
141
+ for (; offset < len; offset++) {
142
+ const code = arg.charCodeAt(offset);
143
+ if (code > 0x7F) break;
144
+ mem[ptr + offset] = code;
145
+ }
146
+ if (offset !== len) {
147
+ if (offset !== 0) {
148
+ arg = arg.slice(offset);
149
+ }
150
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
151
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
152
+ const ret = cachedTextEncoder.encodeInto(arg, view);
153
+
154
+ offset += ret.written;
155
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
156
+ }
157
+
158
+ WASM_VECTOR_LEN = offset;
159
+ return ptr;
160
+ }
161
+
162
+ function takeFromExternrefTable0(idx) {
163
+ const value = wasm.__wbindgen_externrefs.get(idx);
164
+ wasm.__externref_table_dealloc(idx);
165
+ return value;
166
+ }
167
+
168
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
169
+ cachedTextDecoder.decode();
170
+ const MAX_SAFARI_DECODE_BYTES = 2146435072;
171
+ let numBytesDecoded = 0;
172
+ function decodeText(ptr, len) {
173
+ numBytesDecoded += len;
174
+ if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
175
+ cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
176
+ cachedTextDecoder.decode();
177
+ numBytesDecoded = len;
178
+ }
179
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
180
+ }
181
+
182
+ const cachedTextEncoder = new TextEncoder();
183
+
184
+ if (!('encodeInto' in cachedTextEncoder)) {
185
+ cachedTextEncoder.encodeInto = function (arg, view) {
186
+ const buf = cachedTextEncoder.encode(arg);
187
+ view.set(buf);
188
+ return {
189
+ read: arg.length,
190
+ written: buf.length
191
+ };
192
+ };
193
+ }
194
+
195
+ let WASM_VECTOR_LEN = 0;
196
+
197
+ let wasmModule, wasmInstance, wasm;
198
+ function __wbg_finalize_init(instance, module) {
199
+ wasmInstance = instance;
200
+ wasm = instance.exports;
201
+ wasmModule = module;
202
+ cachedDataViewMemory0 = null;
203
+ cachedUint8ArrayMemory0 = null;
204
+ wasm.__wbindgen_start();
205
+ return wasm;
206
+ }
207
+
208
+ async function __wbg_load(module, imports) {
209
+ if (typeof Response === 'function' && module instanceof Response) {
210
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
211
+ try {
212
+ return await WebAssembly.instantiateStreaming(module, imports);
213
+ } catch (e) {
214
+ const validResponse = module.ok && expectedResponseType(module.type);
215
+
216
+ if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
217
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
218
+
219
+ } else { throw e; }
220
+ }
221
+ }
222
+
223
+ const bytes = await module.arrayBuffer();
224
+ return await WebAssembly.instantiate(bytes, imports);
225
+ } else {
226
+ const instance = await WebAssembly.instantiate(module, imports);
227
+
228
+ if (instance instanceof WebAssembly.Instance) {
229
+ return { instance, module };
230
+ } else {
231
+ return instance;
232
+ }
233
+ }
234
+
235
+ function expectedResponseType(type) {
236
+ switch (type) {
237
+ case 'basic': case 'cors': case 'default': return true;
238
+ }
239
+ return false;
240
+ }
241
+ }
242
+
243
+ function initSync(module) {
244
+ if (wasm !== undefined) return wasm;
245
+
246
+
247
+ if (module !== undefined) {
248
+ if (Object.getPrototypeOf(module) === Object.prototype) {
249
+ ({module} = module)
250
+ } else {
251
+ console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
252
+ }
253
+ }
254
+
255
+ const imports = __wbg_get_imports();
256
+ if (!(module instanceof WebAssembly.Module)) {
257
+ module = new WebAssembly.Module(module);
258
+ }
259
+ const instance = new WebAssembly.Instance(module, imports);
260
+ return __wbg_finalize_init(instance, module);
261
+ }
262
+
263
+ async function __wbg_init(module_or_path) {
264
+ if (wasm !== undefined) return wasm;
265
+
266
+
267
+ if (module_or_path !== undefined) {
268
+ if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
269
+ ({module_or_path} = module_or_path)
270
+ } else {
271
+ console.warn('using deprecated parameters for the initialization function; pass a single object instead')
272
+ }
273
+ }
274
+
275
+ if (module_or_path === undefined) {
276
+ module_or_path = new URL('turbo_dom_parser_bg.wasm', import.meta.url);
277
+ }
278
+ const imports = __wbg_get_imports();
279
+
280
+ if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
281
+ module_or_path = fetch(module_or_path);
282
+ }
283
+
284
+ const { instance, module } = await __wbg_load(await module_or_path, imports);
285
+
286
+ return __wbg_finalize_init(instance, module);
287
+ }
288
+
289
+ export { initSync, __wbg_init as default };
Binary file
@@ -0,0 +1,11 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const parse: (a: number, b: number) => [number, number, number];
5
+ export const parseBuffer: (a: number, b: number) => [number, number, number];
6
+ export const parseFragment: (a: number, b: number, c: number, d: number) => [number, number, number];
7
+ export const __wbindgen_malloc: (a: number, b: number) => number;
8
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
9
+ export const __wbindgen_externrefs: WebAssembly.Table;
10
+ export const __externref_table_dealloc: (a: number) => void;
11
+ export const __wbindgen_start: () => void;
package/src/core.rs CHANGED
@@ -20,7 +20,7 @@ pub const DOCUMENT_NODE: u8 = 9;
20
20
  pub const DOCUMENT_TYPE_NODE: u8 = 10;
21
21
  pub const DOCUMENT_FRAGMENT_NODE: u8 = 11;
22
22
 
23
- #[cfg_attr(feature = "wasm-bind", derive(serde::Serialize))]
23
+ #[cfg_attr(feature = "wasm-bind", derive(serde::Serialize), serde(rename_all = "camelCase"))]
24
24
  #[derive(Debug, Clone)]
25
25
  pub struct Attr {
26
26
  pub name: String,
@@ -30,7 +30,7 @@ pub struct Attr {
30
30
  }
31
31
 
32
32
  /// One marshaled DOM node. Nested children = complete tree, no lazy indices.
33
- #[cfg_attr(feature = "wasm-bind", derive(serde::Serialize))]
33
+ #[cfg_attr(feature = "wasm-bind", derive(serde::Serialize), serde(rename_all = "camelCase"))]
34
34
  #[derive(Debug, Clone)]
35
35
  pub struct Node {
36
36
  /// DOM nodeType.
@@ -73,7 +73,7 @@ pub fn parse_html_document(html: &str) -> Node {
73
73
  // the arrays and inflates node objects only on access (no eager full-tree alloc).
74
74
 
75
75
  /// Index-addressed flat tree. Node index IS its id; node 0 is the document.
76
- #[cfg_attr(feature = "wasm-bind", derive(serde::Serialize))]
76
+ #[cfg_attr(feature = "wasm-bind", derive(serde::Serialize), serde(rename_all = "camelCase"))]
77
77
  #[derive(Default)]
78
78
  pub struct Soa {
79
79
  pub node_type: Vec<u8>, // DOM nodeType
@@ -42,7 +42,27 @@ export function unpack(soa) {
42
42
  tagNames, attrNames, attrPrefixes, attrValues, strings,
43
43
  };
44
44
  }
45
- const STR_DECODER = new TextDecoder();
45
+ // Decode the UTF-8 string blob. Prefer the platform TextDecoder (Node/browser);
46
+ // fall back to a tiny inline UTF-8 decoder so turbo-dom loads in a bare V8 with no
47
+ // web-platform globals (the native/Node path keeps using the real TextDecoder —
48
+ // this branch is never taken there).
49
+ export function utf8Decode(bytes) {
50
+ let out = '', i = 0;
51
+ const n = bytes.length;
52
+ while (i < n) {
53
+ const c = bytes[i++];
54
+ if (c < 0x80) out += String.fromCharCode(c);
55
+ else if (c < 0xe0) out += String.fromCharCode(((c & 0x1f) << 6) | (bytes[i++] & 0x3f));
56
+ else if (c < 0xf0) out += String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f));
57
+ else {
58
+ let cp = ((c & 0x07) << 18) | ((bytes[i++] & 0x3f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f);
59
+ cp -= 0x10000;
60
+ out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ const STR_DECODER = typeof TextDecoder !== 'undefined' ? new TextDecoder() : { decode: utf8Decode };
46
66
 
47
67
  export class Buffer {
48
68
  constructor(soa) {
@@ -111,7 +111,14 @@ function buildIndex(scope) {
111
111
  const styles = scope.getElementsByTagName('style');
112
112
  const rules = [];
113
113
  let order = 0;
114
- for (let i = 0; i < styles.length; i++) order = parseStylesheet(styles[i].textContent || '', order, rules);
114
+ for (let i = 0; i < styles.length; i++) {
115
+ order = parseStylesheet(styles[i].textContent || '', order, rules);
116
+ // CSS-in-JS engines inject via sheet.insertRule(), which never touches
117
+ // textContent. Read those rules too (only present on emotion-touched <style>s;
118
+ // __sheet is undefined otherwise, so plain stylesheets pay nothing here).
119
+ const sheet = styles[i].__sheet;
120
+ if (sheet) for (let k = 0; k < sheet.cssRules.length; k++) order = parseStylesheet(sheet.cssRules[k].cssText, order, rules);
121
+ }
115
122
  return rules;
116
123
  }
117
124
 
@@ -0,0 +1,109 @@
1
+ // Minimal CSSOM — just enough for CSS-in-JS engines (emotion / styled-components /
2
+ // MUI) to inject rules at runtime without throwing, AND to feed those rules into
3
+ // the partial cascade (cascade.mjs) so getComputedStyle resolves them.
4
+ //
5
+ // emotion's `sheetForTag` reads `tag.sheet` and `document.styleSheets`, then calls
6
+ // `sheet.insertRule(rule, index)` per CSS rule. It only needs insertRule to not
7
+ // throw and `cssRules.length` to be readable — but we go one cheap step further:
8
+ // insertRule bumps Document.__version (via the owner <style>'s __touch), so the
9
+ // version-keyed style index in cascade.mjs rebuilds and picks the rules up.
10
+ //
11
+ // HONEST: this is a rule STORE, not a CSS engine. No serialization normalization,
12
+ // no @media evaluation, no specificity beyond what cascade.mjs already does. Rules
13
+ // are kept as their raw cssText; cascade.mjs parses them the same way it parses a
14
+ // <style>'s textContent.
15
+
16
+ export class CSSStyleRule {
17
+ constructor(cssText) {
18
+ this.cssText = String(cssText);
19
+ this.type = 1; // CSSRule.STYLE_RULE
20
+ }
21
+ get selectorText() {
22
+ const i = this.cssText.indexOf('{');
23
+ return i < 0 ? '' : this.cssText.slice(0, i).trim();
24
+ }
25
+ }
26
+
27
+ // Split a stylesheet string into top-level rule strings (brace-depth aware), used
28
+ // by replaceSync/constructor text. Mirrors cascade.mjs's depth scan but keeps the
29
+ // braces so each piece is a self-contained cssText.
30
+ function splitTopLevelRules(css) {
31
+ const out = [];
32
+ const n = css.length;
33
+ let i = 0;
34
+ while (i < n) {
35
+ let j = i;
36
+ while (j < n && css[j] !== '{' && css[j] !== '}') j++;
37
+ if (j >= n) break;
38
+ if (css[j] === '}') { i = j + 1; continue; }
39
+ let depth = 1, k = j + 1;
40
+ while (k < n && depth > 0) { const ch = css[k]; if (ch === '{') depth++; else if (ch === '}') depth--; k++; }
41
+ const piece = css.slice(i, k).trim();
42
+ if (piece) out.push(piece);
43
+ i = k;
44
+ }
45
+ return out;
46
+ }
47
+
48
+ export class CSSStyleSheet {
49
+ // ownerNode is the <style> element (null for constructed/adopted sheets).
50
+ constructor(ownerNode = null, init = {}) {
51
+ this.ownerNode = ownerNode;
52
+ this.cssRules = [];
53
+ this.type = 'text/css';
54
+ this.disabled = false;
55
+ this.media = init.media != null ? init.media : [];
56
+ this.title = init.title != null ? init.title : null;
57
+ this.href = null;
58
+ this.parentStyleSheet = null;
59
+ this.ownerRule = null;
60
+ }
61
+
62
+ // legacy alias some libraries read
63
+ get rules() { return this.cssRules; }
64
+
65
+ insertRule(rule, index = 0) {
66
+ const i = index === undefined ? 0 : index;
67
+ if (i < 0 || i > this.cssRules.length) throw new RangeError('insertRule: index out of bounds');
68
+ this.cssRules.splice(i, 0, new CSSStyleRule(rule));
69
+ this.#invalidate();
70
+ return i;
71
+ }
72
+
73
+ deleteRule(index) {
74
+ if (index < 0 || index >= this.cssRules.length) throw new RangeError('deleteRule: index out of bounds');
75
+ this.cssRules.splice(index, 1);
76
+ this.#invalidate();
77
+ }
78
+
79
+ // legacy IE-era APIs (some older libs still call these)
80
+ addRule(selector = '', style = '', index) {
81
+ const i = index === undefined ? this.cssRules.length : index;
82
+ this.insertRule(`${selector} { ${style} }`, i);
83
+ return -1; // spec'd legacy return
84
+ }
85
+ removeRule(index = 0) { this.deleteRule(index); }
86
+
87
+ // constructable-stylesheet API (adoptedStyleSheets); replaceSync is synchronous.
88
+ replaceSync(text) {
89
+ this.cssRules = splitTopLevelRules(String(text)).map((r) => new CSSStyleRule(r));
90
+ this.#invalidate();
91
+ }
92
+ replace(text) { this.replaceSync(text); return Promise.resolve(this); }
93
+
94
+ // Bump the owning Document's version so cascade.mjs's version-keyed style index
95
+ // (and any getComputedStyle cache) rebuilds. Constructed sheets (no ownerNode)
96
+ // have nothing to invalidate until adopted — a no-op, which is correct.
97
+ #invalidate() {
98
+ const n = this.ownerNode;
99
+ if (n && n.__touch) n.__touch();
100
+ }
101
+ }
102
+
103
+ // Live StyleSheetList for document.styleSheets: a plain array (indexable +
104
+ // iterable, what emotion needs) with the spec's `item()` accessor bolted on.
105
+ export function styleSheetList(sheets) {
106
+ const list = sheets.slice();
107
+ list.item = (i) => list[i] ?? null;
108
+ return list;
109
+ }
@@ -6,7 +6,7 @@
6
6
  // Mutation promotes the affected node to fully-owned (COW). Reads are transparent
7
7
  // across the boundary — a buffer-backed read and an owned read are indistinguishable.
8
8
 
9
- import { createRequire } from 'node:module';
9
+ import { getParser } from './parser.mjs';
10
10
  import {
11
11
  EventTarget, Event, CustomEvent,
12
12
  UIEvent, MouseEvent, KeyboardEvent, FocusEvent,
@@ -14,6 +14,8 @@ import {
14
14
  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
+ import { CSSStyleSheet, styleSheetList } from './cssom.mjs';
18
+ import { SVGAnimatedString, SVGAnimatedLength, SVGAnimatedRect, SVG_LENGTH_ATTRS } from './svg.mjs';
17
19
 
18
20
  // Per-node query-result cache keyed by (selector, document version). querySelectorAll
19
21
  // returns a STATIC list per spec, so caching is safe until the next mutation bumps
@@ -56,9 +58,6 @@ function cachedQS(node, sel) {
56
58
  import { Buffer } from './buffer.mjs';
57
59
  import { makeCanvasStub } from './stubs.mjs';
58
60
 
59
- const require = createRequire(import.meta.url);
60
- const native = require('../../index.js');
61
-
62
61
  export const ELEMENT_NODE = 1;
63
62
  export const TEXT_NODE = 3;
64
63
  export const COMMENT_NODE = 8;
@@ -379,6 +378,7 @@ export class Element extends Node {
379
378
  this.__qsCache = undefined; // querySelector cache
380
379
  this.__computedStyle = undefined; // getComputedStyle snapshot (version-keyed)
381
380
  this.__shadow = undefined; // attached shadow root (open or closed)
381
+ this.__sheet = undefined; // CSSOM CSSStyleSheet for <style> (lazy, CSS-in-JS)
382
382
  }
383
383
 
384
384
  get nodeType() { return ELEMENT_NODE; }
@@ -476,6 +476,16 @@ export class Element extends Node {
476
476
  return this.__dataset;
477
477
  }
478
478
 
479
+ // CSSOM seam for CSS-in-JS (emotion/styled-components/MUI): a <style> exposes a
480
+ // CSSStyleSheet so engines can insertRule() at runtime. Created lazily on first
481
+ // read (only emotion-touched <style>s ever get one), so plain documents pay
482
+ // nothing. Rules inserted here flow into the cascade via cascade.mjs reading
483
+ // this.__sheet. Non-<style> elements have no sheet (null), matching the platform.
484
+ get sheet() {
485
+ if (this.localName !== 'style') return null;
486
+ return this.__sheet ?? (this.__sheet = new CSSStyleSheet(this));
487
+ }
488
+
479
489
  // ---- form-control properties (on the prototype so libraries that read
480
490
  // element.constructor.prototype descriptors — e.g. user-event — find them) ----
481
491
  get value() {
@@ -686,7 +696,7 @@ export class Element extends Node {
686
696
  // ---- innerHTML / outerHTML ----
687
697
  get innerHTML() { return serializeInner(this); }
688
698
  set innerHTML(html) {
689
- const frag = native.parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
699
+ const frag = getParser().parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
690
700
  this.__kids = [];
691
701
  this.__touch();
692
702
  for (const rawChild of frag.children) {
@@ -715,7 +725,7 @@ export class Element extends Node {
715
725
  }
716
726
 
717
727
  cloneNode(deep = false) {
718
- const el = new Element(this.ownerDocument, this.localName, this.__ns);
728
+ const el = newElement(this.ownerDocument, this.localName, this.__ns);
719
729
  el.__attrs = (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => ({ ...a }));
720
730
  if (deep) {
721
731
  // build the cloned child array directly — the clone is detached, so per-child
@@ -926,6 +936,46 @@ export class Element extends Node {
926
936
  }
927
937
 
928
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
+
929
979
  // ---------------------------------------------------- DocumentFragment ----
930
980
  export class DocumentFragment extends Node {
931
981
  get nodeType() { return DOCUMENT_FRAGMENT_NODE; }
@@ -960,7 +1010,7 @@ export class ShadowRoot extends DocumentFragment {
960
1010
  get nodeName() { return '#document-fragment'; }
961
1011
  get innerHTML() { return serializeInner(this); }
962
1012
  set innerHTML(html) {
963
- const frag = native.parseFragment(String(html), ''); // empty context → body
1013
+ const frag = getParser().parseFragment(String(html), ''); // empty context → body
964
1014
  this.__kids = [];
965
1015
  this.__touch();
966
1016
  for (const rawChild of frag.children) {
@@ -1464,7 +1514,7 @@ export class Document extends Node {
1464
1514
  switch (nt) {
1465
1515
  case ELEMENT_NODE: {
1466
1516
  const tag = buf.tagName(idx); // read once (was read twice — ctor + template check)
1467
- node = new Element(this, tag, buf.ns(idx));
1517
+ node = newElement(this, tag, buf.ns(idx));
1468
1518
  node.__idx = idx;
1469
1519
  node.__attrIdx = idx; // attrs lazy (constructor left __attrs undefined)
1470
1520
  // template content fragment: a child node typed 11 named "content"
@@ -1496,7 +1546,7 @@ export class Document extends Node {
1496
1546
  let node;
1497
1547
  switch (raw.nodeType) {
1498
1548
  case ELEMENT_NODE:
1499
- node = new Element(this, raw.name, raw.namespace || '');
1549
+ node = newElement(this, raw.name, raw.namespace || '');
1500
1550
  node.__attrs = raw.attrs.map((a) => ({ name: a.name, value: a.value, prefix: a.prefix || '' }));
1501
1551
  if (raw.name === 'template') {
1502
1552
  const contentRaw = raw.children.find((c) => c.nodeType === DOCUMENT_FRAGMENT_NODE && c.name === 'content');
@@ -1543,12 +1593,22 @@ export class Document extends Node {
1543
1593
  get activeElement() { return this.__active || this.body || null; }
1544
1594
  __setActive(el) { this.__active = el; }
1545
1595
 
1596
+ // Live StyleSheetList. emotion iterates this to find its own <style>'s sheet by
1597
+ // ownerNode. Touching `.sheet` here lazily creates a CSSStyleSheet per <style>
1598
+ // (test-time only — never on parse/query paths).
1599
+ get styleSheets() {
1600
+ const styles = this.getElementsByTagName('style');
1601
+ const sheets = [];
1602
+ for (let i = 0; i < styles.length; i++) sheets.push(styles[i].sheet);
1603
+ return styleSheetList(sheets);
1604
+ }
1605
+
1546
1606
  // ---- factories (owned nodes, no buffer) ----
1547
1607
  createElement(tag) { return new Element(this, String(tag).toLowerCase(), ''); }
1548
1608
  createElementNS(ns, qualified) {
1549
1609
  const short = ns === SVG_NS ? 'svg' : ns === MATHML_NS ? 'math' : '';
1550
1610
  const local = qualified.includes(':') ? qualified.split(':')[1] : qualified;
1551
- return new Element(this, local, short);
1611
+ return newElement(this, local, short);
1552
1612
  }
1553
1613
  createTextNode(data) { return new Text(this, String(data)); }
1554
1614
  createComment(data) { return new Comment(this, String(data)); }
@@ -1699,7 +1759,7 @@ export { Event, CustomEvent };
1699
1759
 
1700
1760
  // Parse an HTML string into a fresh Document over the immutable SoA buffer.
1701
1761
  export function parseDocument(html) {
1702
- const soa = native.parseBuffer(String(html));
1762
+ const soa = getParser().parseBuffer(String(html));
1703
1763
  const doc = new Document();
1704
1764
  doc.__load(soa);
1705
1765
  return doc;
@@ -6,17 +6,18 @@
6
6
  // env.reset(); // Layer 5: cheap per-file reset
7
7
  // env.reset('<body>next</body>');
8
8
 
9
- import { createRequire } from 'node:module';
10
9
  import { Document } from './dom.mjs';
11
10
  import { createWindow } from './window.mjs';
12
11
  import { unpack } from './buffer.mjs';
13
-
14
- const require = createRequire(import.meta.url);
15
- const native = require('../../index.js');
12
+ import { getParser, setParserMode } from './parser.mjs';
16
13
 
17
14
  export { Document } from './dom.mjs';
18
15
  export * from './dom.mjs';
19
16
 
17
+ // Parser-selection seam (public API): inject a ready binding for a node-free host,
18
+ // or force 'wasm' | 'native' | 'auto'. See parser.mjs.
19
+ export { setParser, setParserMode, getParser } from './parser.mjs';
20
+
20
21
  // Parse cache: the SoA buffer is READ-ONLY (every mutation goes to a Document's
21
22
  // own __kids/__attrs/__cache overlay, never the buffer), so the same buffer can
22
23
  // back many Documents. Test suites call setup with the SAME html per file
@@ -34,7 +35,7 @@ function parseBufferCached(html) {
34
35
  if (html !== __parseCacheMRU) { __parseCache.delete(html); __parseCache.set(html, hit); __parseCacheMRU = html; }
35
36
  return hit;
36
37
  }
37
- let soa = native.parseBuffer(html);
38
+ let soa = getParser().parseBuffer(html);
38
39
  // Unpack the packed blob into typed-array views ONCE here, not per Document in
39
40
  // every Buffer ctor — the views are read-only over the shared immutable buffer,
40
41
  // so all Documents backed by this cached soa reuse them (no re-unpack per file).
@@ -65,6 +66,12 @@ function promoteDeclarativeShadowRoots(document) {
65
66
  }
66
67
 
67
68
  export function createEnvironment(html = '<!doctype html><html><head></head><body></body></html>', options = {}) {
69
+ // Optional parser selection: options.parser === 'wasm' | 'native' | 'auto'. Applied
70
+ // before the first parse below; process-global (the parser binding is a singleton),
71
+ // so the last mode set wins. Embedders wanting a fully node-free path inject a
72
+ // binding via setParser()/globalThis.__TURBO_DOM_PARSER__ instead.
73
+ if (options.parser) setParserMode(options.parser);
74
+
68
75
  // Layer 1: native parse → immutable SoA buffer (typed arrays, one boundary copy).
69
76
  let currentHtml = String(html);
70
77
  let soa = parseBufferCached(currentHtml);
@@ -0,0 +1,68 @@
1
+ // Parser binding registry for the whole runtime (dom.mjs, index.mjs).
2
+ //
3
+ // Resolution order, lazily on the FIRST parse (getParser):
4
+ // 1. an explicitly injected binding — setParser(b) or globalThis.__TURBO_DOM_PARSER__.
5
+ // This is the node-free path: an embedder (e.g. a non-Node V8 host) instantiates
6
+ // the wasm itself and injects the {parse,parseBuffer,parseFragment} object, so
7
+ // NONE of the Node loaders below ever run.
8
+ // 2. an explicit mode — setParserMode(m), the `parser` option on createEnvironment,
9
+ // TURBO_DOM_PARSER env, or globalThis.__TURBO_DOM_PARSER_MODE__:
10
+ // 'wasm' loads pkg/, 'native' loads the prebuilt .node addon.
11
+ // 3. auto (default): native addon first, wasm fallback.
12
+ //
13
+ // node:module is imported via GUARDED top-level await, so this module still LOADS in
14
+ // a bare V8 with no Node builtins — provided a parser is injected before the first
15
+ // parse, the Node `require` loaders are never reached. (Node hosts get it normally.)
16
+
17
+ let _createRequire;
18
+ try { ({ createRequire: _createRequire } = await import('node:module')); } catch { /* bare V8: parser must be injected */ }
19
+
20
+ let _parser = null;
21
+ let _mode = null; // 'wasm' | 'native' | 'auto' | null (consult env/global)
22
+
23
+ function req(spec) {
24
+ if (!_createRequire) {
25
+ throw new Error(
26
+ 'turbo-dom: no parser available — node:module is missing (bare V8) and nothing was injected. ' +
27
+ 'Call setParser(binding) (or set globalThis.__TURBO_DOM_PARSER__) before the first parse.',
28
+ );
29
+ }
30
+ return _createRequire(import.meta.url)(spec);
31
+ }
32
+ const loadNative = () => req('../../index.js');
33
+ const loadWasm = () => req('../../pkg/turbo_dom_parser.js');
34
+
35
+ // Inject a ready parser binding ({parse, parseBuffer, parseFragment}). Pass null to
36
+ // clear it (next getParser re-resolves). The node-free seam for embedders.
37
+ export function setParser(binding) { _parser = binding || null; }
38
+
39
+ // Force a parser mode: 'wasm' | 'native' | 'auto' (or null to consult env/global).
40
+ // Resets the resolved parser so the next parse honors the new mode.
41
+ export function setParserMode(mode) {
42
+ _mode = mode || null;
43
+ _parser = null;
44
+ }
45
+
46
+ function resolveMode() {
47
+ if (_mode) return _mode;
48
+ const g = typeof globalThis !== 'undefined' ? globalThis : {};
49
+ if (g.__TURBO_DOM_PARSER_MODE__) return g.__TURBO_DOM_PARSER_MODE__;
50
+ if (typeof process !== 'undefined' && process.env && process.env.TURBO_DOM_PARSER) return process.env.TURBO_DOM_PARSER;
51
+ return 'auto';
52
+ }
53
+
54
+ // Resolve (once) and return the active parser binding. Memoized — every call after
55
+ // the first is a single field read, so call sites can use getParser().parseBuffer(…)
56
+ // freely without caching it themselves.
57
+ export function getParser() {
58
+ if (_parser) return _parser;
59
+ const g = typeof globalThis !== 'undefined' ? globalThis : {};
60
+ if (g.__TURBO_DOM_PARSER__) return (_parser = g.__TURBO_DOM_PARSER__);
61
+ const mode = resolveMode();
62
+ if (mode === 'wasm') return (_parser = loadWasm());
63
+ if (mode === 'native') return (_parser = loadNative());
64
+ // auto: native addon first, wasm fallback; surface the native error if both fail.
65
+ try { _parser = loadNative(); }
66
+ catch (nativeErr) { try { _parser = loadWasm(); } catch { throw nativeErr; } }
67
+ return _parser;
68
+ }
@@ -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
+ ];
@@ -11,8 +11,9 @@ import {
11
11
  makeLocation, makeHistory,
12
12
  } from './stubs.mjs';
13
13
  import { makeGetComputedStyle } from './cascade.mjs';
14
+ import { CSSStyleSheet } from './cssom.mjs';
14
15
  import {
15
- Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
16
+ Node, Element, SVGElement, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
16
17
  MutationObserver, DOMParser, XMLSerializer, ShadowRoot,
17
18
  } from './dom.mjs';
18
19
  import {
@@ -205,16 +206,33 @@ function performanceNow() {
205
206
  return nodePerformance.now();
206
207
  }
207
208
 
209
+ // Resolve the host base classes ONCE, with minimal fallbacks so turbo-dom loads &
210
+ // runs in a bare V8 lacking web-platform globals. Where the platform provides the
211
+ // real class (Node/browser) we extend it and keep full behavior; the fallbacks are
212
+ // only enough to not throw at load and to round-trip the few fields turbo-dom uses.
213
+ // Embedders wanting full URL parsing / Blob semantics should still polyfill the
214
+ // host globals — these are honest minimums, not a spec implementation.
215
+ const URLBase = typeof URL !== 'undefined' ? URL : class URL {
216
+ constructor(url, base) { this.href = base ? `${base}${url}` : String(url); }
217
+ toString() { return this.href; }
218
+ get origin() { return ''; }
219
+ };
220
+ const BlobBase = typeof globalThis.Blob !== 'undefined' ? globalThis.Blob : class Blob {
221
+ constructor(parts = [], opts = {}) { this.__parts = parts; this.type = opts.type || ''; this.size = 0; }
222
+ async text() { return (this.__parts || []).join(''); }
223
+ async arrayBuffer() { return new ArrayBuffer(0); }
224
+ slice() { return new BlobBase(); }
225
+ };
226
+
208
227
  let __objUrlSeq = 0;
209
228
  function makeURL() {
210
- class TurboURL extends URL {}
229
+ class TurboURL extends URLBase {}
211
230
  TurboURL.createObjectURL = () => `blob:turbo-dom/${++__objUrlSeq}`;
212
231
  TurboURL.revokeObjectURL = () => {};
213
232
  return TurboURL;
214
233
  }
215
234
  function makeFile() {
216
- const B = globalThis.Blob;
217
- return class File extends B {
235
+ return class File extends BlobBase {
218
236
  constructor(bits = [], name = 'file', opts = {}) { super(bits, opts); this.name = String(name); this.lastModified = opts.lastModified || 0; }
219
237
  };
220
238
  }
@@ -264,7 +282,7 @@ const STATIC_BASE = {
264
282
  DataTransfer,
265
283
  // every element is a plain Element → `el instanceof HTMLElement` is true.
266
284
  // Tag-specific interfaces match by localName via Symbol.hasInstance.
267
- HTMLElement: Element, SVGElement: Element,
285
+ HTMLElement: Element, SVGElement,
268
286
  // SVG interface globals. Libs (and test setup mocks) do
269
287
  // `Object.defineProperty(SVGSVGElement.prototype, 'viewBox', …)` and
270
288
  // `el instanceof SVGSVGElement`; an undefined global is a hard ReferenceError.
@@ -317,9 +335,9 @@ const STATIC_BASE = {
317
335
  HTMLDataElement: tagClass('data'), HTMLTimeElement: tagClass('time'),
318
336
  HTMLSlotElement: tagClass('slot'), HTMLMenuElement: tagClass('menu'),
319
337
  HTMLDocument: Document,
320
- MutationObserver, DOMParser, XMLSerializer,
321
- URL: TURBO_URL, URLSearchParams,
322
- Blob: globalThis.Blob, File: TURBO_FILE, FileReader,
338
+ MutationObserver, DOMParser, XMLSerializer, CSSStyleSheet,
339
+ URL: TURBO_URL, URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : undefined,
340
+ Blob: BlobBase, File: TURBO_FILE, FileReader,
323
341
  AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
324
342
  TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
325
343
  fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
Binary file