@miaskiewicz/turbo-dom 0.1.26 → 0.1.28
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/index.d.ts +4 -14
- package/package.json +1 -1
- package/src/core.rs +16 -3
- package/src/lib.rs +27 -28
- package/src/runtime/buffer.mjs +45 -4
- package/src/runtime/dom.mjs +12 -2
- package/src/runtime/html-serialize.mjs +4 -1
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
package/index.d.ts
CHANGED
|
@@ -34,23 +34,13 @@ export declare function parseRaw(html: string): number
|
|
|
34
34
|
* objects lazily from this — no eager full-tree allocation. The fast path.
|
|
35
35
|
*/
|
|
36
36
|
export interface JsSoa {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
parent: Int32Array
|
|
41
|
-
firstChild: Int32Array
|
|
42
|
-
nextSib: Int32Array
|
|
43
|
-
textId: Int32Array
|
|
44
|
-
pubId: Int32Array
|
|
45
|
-
sysId: Int32Array
|
|
46
|
-
attrStart: Int32Array
|
|
47
|
-
attrCount: Uint16Array
|
|
48
|
-
attrNameId: Uint32Array
|
|
49
|
-
attrValue: Array<string>
|
|
50
|
-
attrPrefixId: Uint32Array
|
|
37
|
+
packed: Uint8Array
|
|
38
|
+
n: number
|
|
39
|
+
m: number
|
|
51
40
|
tagNames: Array<string>
|
|
52
41
|
attrNames: Array<string>
|
|
53
42
|
attrPrefixes: Array<string>
|
|
43
|
+
attrValues: Array<string>
|
|
54
44
|
strings: Array<string>
|
|
55
45
|
}
|
|
56
46
|
/** Parse a document into the SoA flat buffer (the fast runtime path). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
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",
|
package/src/core.rs
CHANGED
|
@@ -89,12 +89,13 @@ pub struct Soa {
|
|
|
89
89
|
pub attr_count: Vec<u16>, // attrs for this node
|
|
90
90
|
// flat attr tables — names/prefixes interned (highly repetitive), values pooled
|
|
91
91
|
pub attr_name_id: Vec<u32>, // index into attr_names
|
|
92
|
-
pub
|
|
92
|
+
pub attr_value_id: Vec<u32>, // index into attr_values (interned, deduped)
|
|
93
93
|
pub attr_prefix_id: Vec<u32>, // index into attr_prefixes
|
|
94
94
|
// string tables (interned, deduped)
|
|
95
95
|
pub tag_names: Vec<String>,
|
|
96
96
|
pub attr_names: Vec<String>,
|
|
97
97
|
pub attr_prefixes: Vec<String>,
|
|
98
|
+
pub attr_values: Vec<String>, // interned attr value dictionary
|
|
98
99
|
pub strings: Vec<String>, // text/comment/doctype data, pooled
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -103,6 +104,7 @@ struct SoaBuilder {
|
|
|
103
104
|
tag_map: std::collections::HashMap<String, u32>,
|
|
104
105
|
attr_name_map: std::collections::HashMap<String, u32>,
|
|
105
106
|
attr_prefix_map: std::collections::HashMap<String, u32>,
|
|
107
|
+
attr_value_map: std::collections::HashMap<String, u32>,
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
impl SoaBuilder {
|
|
@@ -133,6 +135,15 @@ impl SoaBuilder {
|
|
|
133
135
|
self.attr_prefix_map.insert(prefix.to_string(), id);
|
|
134
136
|
id
|
|
135
137
|
}
|
|
138
|
+
fn intern_attr_value(&mut self, value: &str) -> u32 {
|
|
139
|
+
if let Some(&id) = self.attr_value_map.get(value) {
|
|
140
|
+
return id;
|
|
141
|
+
}
|
|
142
|
+
let id = self.soa.attr_values.len() as u32;
|
|
143
|
+
self.soa.attr_values.push(value.to_string());
|
|
144
|
+
self.attr_value_map.insert(value.to_string(), id);
|
|
145
|
+
id
|
|
146
|
+
}
|
|
136
147
|
fn push_string(&mut self, s: &str) -> i32 {
|
|
137
148
|
let id = self.soa.strings.len() as i32;
|
|
138
149
|
self.soa.strings.push(s.to_string());
|
|
@@ -184,12 +195,13 @@ impl SoaBuilder {
|
|
|
184
195
|
self.soa.tag_id[idx] = self.intern_tag(&name.local);
|
|
185
196
|
let borrowed = attrs.borrow();
|
|
186
197
|
if !borrowed.is_empty() {
|
|
187
|
-
self.soa.attr_start[idx] = self.soa.
|
|
198
|
+
self.soa.attr_start[idx] = self.soa.attr_name_id.len() as i32;
|
|
188
199
|
self.soa.attr_count[idx] = borrowed.len() as u16;
|
|
189
200
|
for attr in borrowed.iter() {
|
|
190
201
|
let nid = self.intern_attr_name(&attr.name.local);
|
|
191
202
|
self.soa.attr_name_id.push(nid);
|
|
192
|
-
self.
|
|
203
|
+
let vid = self.intern_attr_value(&attr.value);
|
|
204
|
+
self.soa.attr_value_id.push(vid);
|
|
193
205
|
let pfx = attr.name.prefix.as_ref().map(|p| p.to_string()).unwrap_or_default();
|
|
194
206
|
let pid = self.intern_attr_prefix(&pfx);
|
|
195
207
|
self.soa.attr_prefix_id.push(pid);
|
|
@@ -252,6 +264,7 @@ pub fn parse_html_soa(html: &str) -> Soa {
|
|
|
252
264
|
tag_map: std::collections::HashMap::new(),
|
|
253
265
|
attr_name_map: std::collections::HashMap::new(),
|
|
254
266
|
attr_prefix_map: std::collections::HashMap::new(),
|
|
267
|
+
attr_value_map: std::collections::HashMap::new(),
|
|
255
268
|
};
|
|
256
269
|
b.alloc(&dom.document, -1);
|
|
257
270
|
b.soa
|
package/src/lib.rs
CHANGED
|
@@ -71,48 +71,47 @@ mod napi_front {
|
|
|
71
71
|
|
|
72
72
|
/// SoA flat buffer: structure as typed arrays, crossed once. JS inflates node
|
|
73
73
|
/// objects lazily from this — no eager full-tree allocation. The fast path.
|
|
74
|
+
// All numeric columns packed into ONE little-endian byte blob (1 ArrayBuffer +
|
|
75
|
+
// zero-copy views in JS, vs 13 separate addon buffers + finalizers per parse).
|
|
76
|
+
// Layout — 4-byte block first (keeps every Int32/Uint32 view 4-aligned), then
|
|
77
|
+
// u16, then u8. Length-N: tag_id,parent,first_child,next_sib,text_id,pub_id,
|
|
78
|
+
// sys_id,attr_start | Length-M: attr_name_id,attr_value_id,attr_prefix_id |
|
|
79
|
+
// u16 N: attr_count | u8 N: node_type, ns. JS unpack must mirror this order.
|
|
74
80
|
#[napi(object)]
|
|
75
81
|
pub struct JsSoa {
|
|
76
|
-
pub
|
|
77
|
-
pub
|
|
78
|
-
pub
|
|
79
|
-
pub parent: Int32Array,
|
|
80
|
-
pub first_child: Int32Array,
|
|
81
|
-
pub next_sib: Int32Array,
|
|
82
|
-
pub text_id: Int32Array,
|
|
83
|
-
pub pub_id: Int32Array,
|
|
84
|
-
pub sys_id: Int32Array,
|
|
85
|
-
pub attr_start: Int32Array,
|
|
86
|
-
pub attr_count: Uint16Array,
|
|
87
|
-
pub attr_name_id: Uint32Array,
|
|
88
|
-
pub attr_value: Vec<String>,
|
|
89
|
-
pub attr_prefix_id: Uint32Array,
|
|
82
|
+
pub packed: Uint8Array,
|
|
83
|
+
pub n: u32,
|
|
84
|
+
pub m: u32,
|
|
90
85
|
pub tag_names: Vec<String>,
|
|
91
86
|
pub attr_names: Vec<String>,
|
|
92
87
|
pub attr_prefixes: Vec<String>,
|
|
88
|
+
pub attr_values: Vec<String>,
|
|
93
89
|
pub strings: Vec<String>,
|
|
94
90
|
}
|
|
95
91
|
|
|
96
92
|
impl From<core::Soa> for JsSoa {
|
|
97
93
|
fn from(s: core::Soa) -> Self {
|
|
94
|
+
let n = s.node_type.len();
|
|
95
|
+
let m = s.attr_name_id.len();
|
|
96
|
+
let mut buf: Vec<u8> = Vec::with_capacity(36 * n + 12 * m);
|
|
97
|
+
for col in [&s.tag_id] { for v in col.iter() { buf.extend_from_slice(&v.to_le_bytes()); } }
|
|
98
|
+
for col in [&s.parent, &s.first_child, &s.next_sib, &s.text_id, &s.pub_id, &s.sys_id, &s.attr_start] {
|
|
99
|
+
for v in col.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
|
|
100
|
+
}
|
|
101
|
+
for col in [&s.attr_name_id, &s.attr_value_id, &s.attr_prefix_id] {
|
|
102
|
+
for v in col.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
|
|
103
|
+
}
|
|
104
|
+
for v in s.attr_count.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
|
|
105
|
+
buf.extend_from_slice(&s.node_type);
|
|
106
|
+
buf.extend_from_slice(&s.ns);
|
|
98
107
|
JsSoa {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
parent: Int32Array::new(s.parent),
|
|
103
|
-
first_child: Int32Array::new(s.first_child),
|
|
104
|
-
next_sib: Int32Array::new(s.next_sib),
|
|
105
|
-
text_id: Int32Array::new(s.text_id),
|
|
106
|
-
pub_id: Int32Array::new(s.pub_id),
|
|
107
|
-
sys_id: Int32Array::new(s.sys_id),
|
|
108
|
-
attr_start: Int32Array::new(s.attr_start),
|
|
109
|
-
attr_count: Uint16Array::new(s.attr_count),
|
|
110
|
-
attr_name_id: Uint32Array::new(s.attr_name_id),
|
|
111
|
-
attr_value: s.attr_value,
|
|
112
|
-
attr_prefix_id: Uint32Array::new(s.attr_prefix_id),
|
|
108
|
+
packed: Uint8Array::new(buf),
|
|
109
|
+
n: n as u32,
|
|
110
|
+
m: m as u32,
|
|
113
111
|
tag_names: s.tag_names,
|
|
114
112
|
attr_names: s.attr_names,
|
|
115
113
|
attr_prefixes: s.attr_prefixes,
|
|
114
|
+
attr_values: s.attr_values,
|
|
116
115
|
strings: s.strings,
|
|
117
116
|
}
|
|
118
117
|
}
|
package/src/runtime/buffer.mjs
CHANGED
|
@@ -3,10 +3,32 @@
|
|
|
3
3
|
|
|
4
4
|
const NS_SHORT = ['', 'svg', 'math'];
|
|
5
5
|
|
|
6
|
+
// Inflate the packed byte blob into named typed-array views (zero-copy).
|
|
7
|
+
// Mirrors the column order in lib.rs JsSoa::from. One ArrayBuffer, ~13 views.
|
|
8
|
+
function unpack(soa) {
|
|
9
|
+
const u8 = soa.packed, ab = u8.buffer, base = u8.byteOffset, n = soa.n, m = soa.m;
|
|
10
|
+
let off = base;
|
|
11
|
+
const i32 = () => { const a = new Int32Array(ab, off, n); off += n * 4; return a; };
|
|
12
|
+
const u32n = () => { const a = new Uint32Array(ab, off, n); off += n * 4; return a; };
|
|
13
|
+
const u32m = () => { const a = new Uint32Array(ab, off, m); off += m * 4; return a; };
|
|
14
|
+
const tagId = u32n(), parent = i32(), firstChild = i32(), nextSib = i32(),
|
|
15
|
+
textId = i32(), pubId = i32(), sysId = i32(), attrStart = i32();
|
|
16
|
+
const attrNameId = u32m(), attrValueId = u32m(), attrPrefixId = u32m();
|
|
17
|
+
const attrCount = new Uint16Array(ab, off, n); off += n * 2;
|
|
18
|
+
const nodeType = new Uint8Array(ab, off, n); off += n;
|
|
19
|
+
const ns = new Uint8Array(ab, off, n); off += n;
|
|
20
|
+
return {
|
|
21
|
+
nodeType, ns, tagId, parent, firstChild, nextSib, textId, pubId, sysId,
|
|
22
|
+
attrStart, attrCount, attrNameId, attrValueId, attrPrefixId,
|
|
23
|
+
tagNames: soa.tagNames, attrNames: soa.attrNames, attrPrefixes: soa.attrPrefixes,
|
|
24
|
+
attrValues: soa.attrValues, strings: soa.strings,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
6
28
|
export class Buffer {
|
|
7
29
|
constructor(soa) {
|
|
8
|
-
this.soa = soa;
|
|
9
|
-
this.length = soa.nodeType.length;
|
|
30
|
+
this.soa = soa.packed ? unpack(soa) : soa;
|
|
31
|
+
this.length = this.soa.nodeType.length;
|
|
10
32
|
}
|
|
11
33
|
nodeType(i) { return this.soa.nodeType[i]; }
|
|
12
34
|
ns(i) { return NS_SHORT[this.soa.ns[i]] || ''; }
|
|
@@ -17,16 +39,35 @@ export class Buffer {
|
|
|
17
39
|
text(i) { const t = this.soa.textId[i]; return t < 0 ? '' : this.soa.strings[t]; }
|
|
18
40
|
publicId(i) { const t = this.soa.pubId[i]; return t < 0 ? '' : this.soa.strings[t]; }
|
|
19
41
|
systemId(i) { const t = this.soa.sysId[i]; return t < 0 ? '' : this.soa.strings[t]; }
|
|
42
|
+
// Read a single attr value / presence straight from the columns — no array,
|
|
43
|
+
// no {name,value,prefix} objects. The hot read path (selectors hammer
|
|
44
|
+
// getAttribute('class')/('id')) never materializes the attr list.
|
|
45
|
+
attrGet(i, name) {
|
|
46
|
+
const start = this.soa.attrStart[i];
|
|
47
|
+
if (start < 0) return null;
|
|
48
|
+
const count = this.soa.attrCount[i];
|
|
49
|
+
const { attrNameId, attrNames, attrValueId, attrValues } = this.soa;
|
|
50
|
+
for (let k = 0; k < count; k++) if (attrNames[attrNameId[start + k]] === name) return attrValues[attrValueId[start + k]];
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
attrHas(i, name) {
|
|
54
|
+
const start = this.soa.attrStart[i];
|
|
55
|
+
if (start < 0) return false;
|
|
56
|
+
const count = this.soa.attrCount[i];
|
|
57
|
+
const { attrNameId, attrNames } = this.soa;
|
|
58
|
+
for (let k = 0; k < count; k++) if (attrNames[attrNameId[start + k]] === name) return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
20
61
|
attrs(i) {
|
|
21
62
|
const start = this.soa.attrStart[i];
|
|
22
63
|
if (start < 0) return [];
|
|
23
64
|
const count = this.soa.attrCount[i];
|
|
24
65
|
const out = new Array(count);
|
|
25
|
-
const { attrNameId, attrNames,
|
|
66
|
+
const { attrNameId, attrNames, attrValueId, attrValues, attrPrefixId, attrPrefixes } = this.soa;
|
|
26
67
|
for (let k = 0; k < count; k++) {
|
|
27
68
|
out[k] = {
|
|
28
69
|
name: attrNames[attrNameId[start + k]],
|
|
29
|
-
value:
|
|
70
|
+
value: attrValues[attrValueId[start + k]],
|
|
30
71
|
prefix: attrPrefixes[attrPrefixId[start + k]] || '',
|
|
31
72
|
};
|
|
32
73
|
}
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -354,8 +354,18 @@ export class Element extends Node {
|
|
|
354
354
|
// builds the array from the SoA only when an attribute is first touched (many
|
|
355
355
|
// elements are inflated for traversal but never have attrs read).
|
|
356
356
|
__buildAttrs() { const doc = this.ownerDocument, buf = doc && doc.__buf; return (this.__attrIdx >= 0 && buf) ? buf.attrs(this.__attrIdx) : []; }
|
|
357
|
-
getAttribute(name) {
|
|
358
|
-
|
|
357
|
+
getAttribute(name) {
|
|
358
|
+
const at = this.__attrs;
|
|
359
|
+
if (at !== undefined) { for (let i = 0; i < at.length; i++) if (at[i].name === name) return at[i].value; return null; }
|
|
360
|
+
const doc = this.ownerDocument, buf = doc && doc.__buf; // lazy: read column, don't materialize
|
|
361
|
+
return (this.__attrIdx >= 0 && buf) ? buf.attrGet(this.__attrIdx, name) : null;
|
|
362
|
+
}
|
|
363
|
+
hasAttribute(name) {
|
|
364
|
+
const at = this.__attrs;
|
|
365
|
+
if (at !== undefined) { for (let i = 0; i < at.length; i++) if (at[i].name === name) return true; return false; }
|
|
366
|
+
const doc = this.ownerDocument, buf = doc && doc.__buf;
|
|
367
|
+
return (this.__attrIdx >= 0 && buf) ? buf.attrHas(this.__attrIdx, name) : false;
|
|
368
|
+
}
|
|
359
369
|
getAttributeNames() { return (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => a.name); }
|
|
360
370
|
setAttribute(name, value) {
|
|
361
371
|
if (this.__attrs === undefined) this.__attrs = this.__buildAttrs();
|
|
@@ -20,7 +20,10 @@ function serializeNode(node, out) {
|
|
|
20
20
|
case 1: { // element
|
|
21
21
|
const tag = node.localName;
|
|
22
22
|
out.push('<' + tag);
|
|
23
|
-
|
|
23
|
+
// __attrs is lazy: buffer-backed elements leave it undefined (store __attrIdx).
|
|
24
|
+
// Build from the SoA on first touch, matching every other __attrs read site.
|
|
25
|
+
const attrs = node.__attrs ?? (node.__attrs = node.__buildAttrs());
|
|
26
|
+
for (const a of attrs) {
|
|
24
27
|
const name = a.prefix ? `${a.prefix}:${a.name}` : a.name;
|
|
25
28
|
out.push(` ${name}="${escapeAttr(a.value)}"`);
|
|
26
29
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|