@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 +31 -0
- package/package.json +23 -4
- package/pkg/turbo_dom_parser.d.ts +11 -0
- package/pkg/turbo_dom_parser.js +197 -0
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg/turbo_dom_parser_bg.wasm.d.ts +11 -0
- package/pkg-web/turbo_dom_parser.d.ts +47 -0
- package/pkg-web/turbo_dom_parser.js +289 -0
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/turbo_dom_parser_bg.wasm.d.ts +11 -0
- package/src/core.rs +3 -3
- package/src/runtime/buffer.mjs +21 -1
- package/src/runtime/cascade.mjs +8 -1
- package/src/runtime/cssom.mjs +109 -0
- package/src/runtime/dom.mjs +71 -11
- package/src/runtime/index.mjs +12 -5
- package/src/runtime/parser.mjs +68 -0
- package/src/runtime/svg.mjs +70 -0
- package/src/runtime/window.mjs +26 -8
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
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.
|
|
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=
|
|
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
|
package/src/runtime/buffer.mjs
CHANGED
|
@@ -42,7 +42,27 @@ export function unpack(soa) {
|
|
|
42
42
|
tagNames, attrNames, attrPrefixes, attrValues, strings,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
|
|
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) {
|
package/src/runtime/cascade.mjs
CHANGED
|
@@ -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++)
|
|
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
|
+
}
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
1762
|
+
const soa = getParser().parseBuffer(String(html));
|
|
1703
1763
|
const doc = new Document();
|
|
1704
1764
|
doc.__load(soa);
|
|
1705
1765
|
return doc;
|
package/src/runtime/index.mjs
CHANGED
|
@@ -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 =
|
|
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
|
+
];
|
package/src/runtime/window.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
Binary file
|