@miaskiewicz/turbo-dom 0.1.57 → 0.1.61

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.61",
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,7 @@ 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';
17
18
 
18
19
  // Per-node query-result cache keyed by (selector, document version). querySelectorAll
19
20
  // returns a STATIC list per spec, so caching is safe until the next mutation bumps
@@ -56,9 +57,6 @@ function cachedQS(node, sel) {
56
57
  import { Buffer } from './buffer.mjs';
57
58
  import { makeCanvasStub } from './stubs.mjs';
58
59
 
59
- const require = createRequire(import.meta.url);
60
- const native = require('../../index.js');
61
-
62
60
  export const ELEMENT_NODE = 1;
63
61
  export const TEXT_NODE = 3;
64
62
  export const COMMENT_NODE = 8;
@@ -379,6 +377,7 @@ export class Element extends Node {
379
377
  this.__qsCache = undefined; // querySelector cache
380
378
  this.__computedStyle = undefined; // getComputedStyle snapshot (version-keyed)
381
379
  this.__shadow = undefined; // attached shadow root (open or closed)
380
+ this.__sheet = undefined; // CSSOM CSSStyleSheet for <style> (lazy, CSS-in-JS)
382
381
  }
383
382
 
384
383
  get nodeType() { return ELEMENT_NODE; }
@@ -476,6 +475,16 @@ export class Element extends Node {
476
475
  return this.__dataset;
477
476
  }
478
477
 
478
+ // CSSOM seam for CSS-in-JS (emotion/styled-components/MUI): a <style> exposes a
479
+ // CSSStyleSheet so engines can insertRule() at runtime. Created lazily on first
480
+ // read (only emotion-touched <style>s ever get one), so plain documents pay
481
+ // nothing. Rules inserted here flow into the cascade via cascade.mjs reading
482
+ // this.__sheet. Non-<style> elements have no sheet (null), matching the platform.
483
+ get sheet() {
484
+ if (this.localName !== 'style') return null;
485
+ return this.__sheet ?? (this.__sheet = new CSSStyleSheet(this));
486
+ }
487
+
479
488
  // ---- form-control properties (on the prototype so libraries that read
480
489
  // element.constructor.prototype descriptors — e.g. user-event — find them) ----
481
490
  get value() {
@@ -686,7 +695,7 @@ export class Element extends Node {
686
695
  // ---- innerHTML / outerHTML ----
687
696
  get innerHTML() { return serializeInner(this); }
688
697
  set innerHTML(html) {
689
- const frag = native.parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
698
+ const frag = getParser().parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
690
699
  this.__kids = [];
691
700
  this.__touch();
692
701
  for (const rawChild of frag.children) {
@@ -960,7 +969,7 @@ export class ShadowRoot extends DocumentFragment {
960
969
  get nodeName() { return '#document-fragment'; }
961
970
  get innerHTML() { return serializeInner(this); }
962
971
  set innerHTML(html) {
963
- const frag = native.parseFragment(String(html), ''); // empty context → body
972
+ const frag = getParser().parseFragment(String(html), ''); // empty context → body
964
973
  this.__kids = [];
965
974
  this.__touch();
966
975
  for (const rawChild of frag.children) {
@@ -1543,6 +1552,16 @@ export class Document extends Node {
1543
1552
  get activeElement() { return this.__active || this.body || null; }
1544
1553
  __setActive(el) { this.__active = el; }
1545
1554
 
1555
+ // Live StyleSheetList. emotion iterates this to find its own <style>'s sheet by
1556
+ // ownerNode. Touching `.sheet` here lazily creates a CSSStyleSheet per <style>
1557
+ // (test-time only — never on parse/query paths).
1558
+ get styleSheets() {
1559
+ const styles = this.getElementsByTagName('style');
1560
+ const sheets = [];
1561
+ for (let i = 0; i < styles.length; i++) sheets.push(styles[i].sheet);
1562
+ return styleSheetList(sheets);
1563
+ }
1564
+
1546
1565
  // ---- factories (owned nodes, no buffer) ----
1547
1566
  createElement(tag) { return new Element(this, String(tag).toLowerCase(), ''); }
1548
1567
  createElementNS(ns, qualified) {
@@ -1699,7 +1718,7 @@ export { Event, CustomEvent };
1699
1718
 
1700
1719
  // Parse an HTML string into a fresh Document over the immutable SoA buffer.
1701
1720
  export function parseDocument(html) {
1702
- const soa = native.parseBuffer(String(html));
1721
+ const soa = getParser().parseBuffer(String(html));
1703
1722
  const doc = new Document();
1704
1723
  doc.__load(soa);
1705
1724
  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
+ }
@@ -11,6 +11,7 @@ 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
16
  Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
16
17
  MutationObserver, DOMParser, XMLSerializer, ShadowRoot,
@@ -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
  }
@@ -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