@miaskiewicz/turbo-dom 0.5.3 → 0.5.5
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/Cargo.lock +3 -2
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/pkg/package.json +1 -1
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/package.json +1 -1
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/src/core.rs +187 -77
- 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/Cargo.lock
CHANGED
|
@@ -541,9 +541,10 @@ dependencies = [
|
|
|
541
541
|
|
|
542
542
|
[[package]]
|
|
543
543
|
name = "turbo-dom"
|
|
544
|
-
version = "0.5.
|
|
544
|
+
version = "0.5.5"
|
|
545
545
|
dependencies = [
|
|
546
546
|
"html5ever",
|
|
547
|
+
"log",
|
|
547
548
|
"markup5ever_rcdom",
|
|
548
549
|
"rustc-hash",
|
|
549
550
|
"serde_json",
|
|
@@ -553,7 +554,7 @@ dependencies = [
|
|
|
553
554
|
|
|
554
555
|
[[package]]
|
|
555
556
|
name = "turbo-dom-parser"
|
|
556
|
-
version = "0.5.
|
|
557
|
+
version = "0.5.5"
|
|
557
558
|
dependencies = [
|
|
558
559
|
"html5ever",
|
|
559
560
|
"markup5ever_rcdom",
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# turbo-dom
|
|
2
2
|
|
|
3
|
-
Current release: **0.5.
|
|
3
|
+
Current release: **0.5.5** (npm package, native parser and Rust DOM crate).
|
|
4
4
|
|
|
5
5
|
A faster, more spec-correct DOM for test runners — a drop-in-style alternative to
|
|
6
6
|
**jsdom** and **happy-dom** for **vitest** and **jest**.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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/pkg/package.json
CHANGED
|
Binary file
|
package/pkg-web/package.json
CHANGED
|
Binary file
|
package/src/core.rs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
use html5ever::driver::ParseOpts;
|
|
9
9
|
use html5ever::tendril::TendrilSink;
|
|
10
10
|
use html5ever::tree_builder::TreeBuilderOpts;
|
|
11
|
-
use html5ever::{parse_document, parse_fragment,
|
|
11
|
+
use html5ever::{ns, parse_document, parse_fragment, LocalName, QualName};
|
|
12
12
|
use markup5ever_rcdom::{Handle, NodeData, RcDom};
|
|
13
13
|
|
|
14
14
|
/// DOM nodeType constants (subset html5ever can emit).
|
|
@@ -20,7 +20,11 @@ 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(
|
|
23
|
+
#[cfg_attr(
|
|
24
|
+
feature = "wasm-bind",
|
|
25
|
+
derive(serde::Serialize),
|
|
26
|
+
serde(rename_all = "camelCase")
|
|
27
|
+
)]
|
|
24
28
|
#[derive(Debug, Clone)]
|
|
25
29
|
pub struct Attr {
|
|
26
30
|
pub name: String,
|
|
@@ -30,7 +34,11 @@ pub struct Attr {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/// One marshaled DOM node. Nested children = complete tree, no lazy indices.
|
|
33
|
-
#[cfg_attr(
|
|
37
|
+
#[cfg_attr(
|
|
38
|
+
feature = "wasm-bind",
|
|
39
|
+
derive(serde::Serialize),
|
|
40
|
+
serde(rename_all = "camelCase")
|
|
41
|
+
)]
|
|
34
42
|
#[derive(Debug, Clone)]
|
|
35
43
|
pub struct Node {
|
|
36
44
|
/// DOM nodeType.
|
|
@@ -53,7 +61,10 @@ pub struct Node {
|
|
|
53
61
|
/// `<noscript>` content is parsed as markup rather than rawtext).
|
|
54
62
|
fn opts() -> ParseOpts {
|
|
55
63
|
ParseOpts {
|
|
56
|
-
tree_builder: TreeBuilderOpts {
|
|
64
|
+
tree_builder: TreeBuilderOpts {
|
|
65
|
+
scripting_enabled: false,
|
|
66
|
+
..Default::default()
|
|
67
|
+
},
|
|
57
68
|
..Default::default()
|
|
58
69
|
}
|
|
59
70
|
}
|
|
@@ -74,7 +85,11 @@ pub fn parse_html_document(html: &str) -> Node {
|
|
|
74
85
|
// the arrays and inflates node objects only on access (no eager full-tree alloc).
|
|
75
86
|
|
|
76
87
|
/// Index-addressed flat tree. Node index IS its id; node 0 is the document.
|
|
77
|
-
#[cfg_attr(
|
|
88
|
+
#[cfg_attr(
|
|
89
|
+
feature = "wasm-bind",
|
|
90
|
+
derive(serde::Serialize),
|
|
91
|
+
serde(rename_all = "camelCase")
|
|
92
|
+
)]
|
|
78
93
|
#[derive(Default)]
|
|
79
94
|
pub struct Soa {
|
|
80
95
|
pub node_type: Vec<u8>, // DOM nodeType
|
|
@@ -89,15 +104,15 @@ pub struct Soa {
|
|
|
89
104
|
pub attr_start: Vec<i32>, // offset into attr_* tables; -1 if none
|
|
90
105
|
pub attr_count: Vec<u16>, // attrs for this node
|
|
91
106
|
// flat attr tables — names/prefixes interned (highly repetitive), values pooled
|
|
92
|
-
pub attr_name_id: Vec<u32>,
|
|
93
|
-
pub attr_value_id: Vec<u32>,
|
|
107
|
+
pub attr_name_id: Vec<u32>, // index into attr_names
|
|
108
|
+
pub attr_value_id: Vec<u32>, // index into attr_values (interned, deduped)
|
|
94
109
|
pub attr_prefix_id: Vec<u32>, // index into attr_prefixes
|
|
95
110
|
// string tables (interned, deduped)
|
|
96
111
|
pub tag_names: Vec<String>,
|
|
97
112
|
pub attr_names: Vec<String>,
|
|
98
113
|
pub attr_prefixes: Vec<String>,
|
|
99
114
|
pub attr_values: Vec<String>, // interned attr value dictionary
|
|
100
|
-
pub strings: Vec<String>,
|
|
115
|
+
pub strings: Vec<String>, // text/comment/doctype data, pooled
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
struct SoaBuilder {
|
|
@@ -152,7 +167,7 @@ impl SoaBuilder {
|
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
// Allocate this node + descendants; return its index.
|
|
155
|
-
fn
|
|
170
|
+
fn alloc_node(&mut self, handle: &Handle, parent: i32) -> (i32, Option<Handle>) {
|
|
156
171
|
let idx = self.soa.node_type.len();
|
|
157
172
|
// push placeholders; fill scalars below
|
|
158
173
|
self.soa.node_type.push(0);
|
|
@@ -171,7 +186,11 @@ impl SoaBuilder {
|
|
|
171
186
|
|
|
172
187
|
match &handle.data {
|
|
173
188
|
NodeData::Document => self.soa.node_type[idx] = DOCUMENT_NODE,
|
|
174
|
-
NodeData::Doctype {
|
|
189
|
+
NodeData::Doctype {
|
|
190
|
+
name,
|
|
191
|
+
public_id,
|
|
192
|
+
system_id,
|
|
193
|
+
} => {
|
|
175
194
|
self.soa.node_type[idx] = DOCUMENT_TYPE_NODE;
|
|
176
195
|
self.soa.text_id[idx] = self.push_string(name);
|
|
177
196
|
self.soa.pub_id[idx] = self.push_string(public_id);
|
|
@@ -187,12 +206,23 @@ impl SoaBuilder {
|
|
|
187
206
|
}
|
|
188
207
|
NodeData::ProcessingInstruction { target, contents } => {
|
|
189
208
|
self.soa.node_type[idx] = PROCESSING_INSTRUCTION_NODE;
|
|
190
|
-
|
|
191
|
-
self.soa.text_id[idx] = self.push_string(
|
|
209
|
+
self.soa.tag_id[idx] = self.intern_tag(target);
|
|
210
|
+
self.soa.text_id[idx] = self.push_string(contents);
|
|
192
211
|
}
|
|
193
|
-
NodeData::Element {
|
|
212
|
+
NodeData::Element {
|
|
213
|
+
name,
|
|
214
|
+
attrs,
|
|
215
|
+
template_contents,
|
|
216
|
+
..
|
|
217
|
+
} => {
|
|
194
218
|
self.soa.node_type[idx] = ELEMENT_NODE;
|
|
195
|
-
self.soa.ns[idx] = if name.ns == ns!(svg) {
|
|
219
|
+
self.soa.ns[idx] = if name.ns == ns!(svg) {
|
|
220
|
+
1
|
|
221
|
+
} else if name.ns == ns!(mathml) {
|
|
222
|
+
2
|
|
223
|
+
} else {
|
|
224
|
+
0
|
|
225
|
+
};
|
|
196
226
|
self.soa.tag_id[idx] = self.intern_tag(&name.local);
|
|
197
227
|
let borrowed = attrs.borrow();
|
|
198
228
|
if !borrowed.is_empty() {
|
|
@@ -203,7 +233,12 @@ impl SoaBuilder {
|
|
|
203
233
|
self.soa.attr_name_id.push(nid);
|
|
204
234
|
let vid = self.intern_attr_value(&attr.value);
|
|
205
235
|
self.soa.attr_value_id.push(vid);
|
|
206
|
-
let pfx = attr
|
|
236
|
+
let pfx = attr
|
|
237
|
+
.name
|
|
238
|
+
.prefix
|
|
239
|
+
.as_ref()
|
|
240
|
+
.map(std::string::ToString::to_string)
|
|
241
|
+
.unwrap_or_default();
|
|
207
242
|
let pid = self.intern_attr_prefix(&pfx);
|
|
208
243
|
self.soa.attr_prefix_id.push(pid);
|
|
209
244
|
}
|
|
@@ -216,47 +251,49 @@ impl SoaBuilder {
|
|
|
216
251
|
}
|
|
217
252
|
}
|
|
218
253
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
(idx as i32, template_content)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn alloc(&mut self, root: &Handle, parent: i32) -> i32 {
|
|
258
|
+
let mut pending = vec![(root.clone(), parent, false)];
|
|
259
|
+
let mut last_child = Vec::new();
|
|
260
|
+
let first = self.soa.node_type.len() as i32;
|
|
261
|
+
while let Some((handle, parent, content)) = pending.pop() {
|
|
262
|
+
let (index, template) = self.alloc_node(&handle, parent);
|
|
263
|
+
last_child.push(-1);
|
|
264
|
+
if content {
|
|
265
|
+
self.soa.node_type[index as usize] = DOCUMENT_FRAGMENT_NODE;
|
|
266
|
+
self.soa.tag_id[index as usize] = self.intern_tag("content");
|
|
267
|
+
}
|
|
268
|
+
if parent >= 0 {
|
|
269
|
+
let previous = last_child[parent as usize];
|
|
270
|
+
if previous < 0 {
|
|
271
|
+
self.soa.first_child[parent as usize] = index;
|
|
272
|
+
} else {
|
|
273
|
+
self.soa.next_sib[previous as usize] = index;
|
|
274
|
+
}
|
|
275
|
+
last_child[parent as usize] = index;
|
|
276
|
+
}
|
|
277
|
+
if let Some(template) = template {
|
|
278
|
+
pending.push((template, index, true));
|
|
279
|
+
}
|
|
280
|
+
for child in handle.children.borrow().iter().rev() {
|
|
281
|
+
pending.push((child.clone(), index, false));
|
|
246
282
|
}
|
|
247
|
-
// link the content fragment as a child of the template
|
|
248
|
-
if prev == -1 { self.soa.first_child[idx] = cidx; } else { self.soa.next_sib[prev as usize] = cidx; }
|
|
249
|
-
prev = cidx;
|
|
250
283
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
idx as i32
|
|
284
|
+
first
|
|
254
285
|
}
|
|
255
286
|
}
|
|
256
287
|
|
|
257
288
|
/// Parse a full document into the `SoA` flat buffer.
|
|
258
289
|
#[must_use]
|
|
259
290
|
pub fn parse_html_soa(html: &str) -> Soa {
|
|
291
|
+
parse_html_soa_with_compat_mode(html).0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Parse once and retain html5ever's document compatibility mode.
|
|
295
|
+
#[must_use]
|
|
296
|
+
pub fn parse_html_soa_with_compat_mode(html: &str) -> (Soa, &'static str) {
|
|
260
297
|
let dom = parse_document(RcDom::default(), opts())
|
|
261
298
|
.from_utf8()
|
|
262
299
|
.read_from(&mut html.as_bytes())
|
|
@@ -266,12 +303,26 @@ pub fn parse_html_soa(html: &str) -> Soa {
|
|
|
266
303
|
// pre-reserve typical intern-table sizes so the build loop doesn't rehash:
|
|
267
304
|
// tag/attr-name sets are small + bounded; values vary more.
|
|
268
305
|
tag_map: rustc_hash::FxHashMap::with_capacity_and_hasher(32, rustc_hash::FxBuildHasher),
|
|
269
|
-
attr_name_map: rustc_hash::FxHashMap::with_capacity_and_hasher(
|
|
270
|
-
|
|
271
|
-
|
|
306
|
+
attr_name_map: rustc_hash::FxHashMap::with_capacity_and_hasher(
|
|
307
|
+
64,
|
|
308
|
+
rustc_hash::FxBuildHasher,
|
|
309
|
+
),
|
|
310
|
+
attr_prefix_map: rustc_hash::FxHashMap::with_capacity_and_hasher(
|
|
311
|
+
8,
|
|
312
|
+
rustc_hash::FxBuildHasher,
|
|
313
|
+
),
|
|
314
|
+
attr_value_map: rustc_hash::FxHashMap::with_capacity_and_hasher(
|
|
315
|
+
128,
|
|
316
|
+
rustc_hash::FxBuildHasher,
|
|
317
|
+
),
|
|
272
318
|
};
|
|
273
319
|
b.alloc(&dom.document, -1);
|
|
274
|
-
|
|
320
|
+
let mode = if dom.quirks_mode.get() == html5ever::tree_builder::QuirksMode::Quirks {
|
|
321
|
+
"BackCompat"
|
|
322
|
+
} else {
|
|
323
|
+
"CSS1Compat"
|
|
324
|
+
};
|
|
325
|
+
(b.soa, mode)
|
|
275
326
|
}
|
|
276
327
|
|
|
277
328
|
/// Parse a document and return only the node count — no `core::Node` tree, no
|
|
@@ -348,18 +399,32 @@ pub fn parse_html_fragment_in(html: &str, context_ns: &str, context_local: &str)
|
|
|
348
399
|
frag
|
|
349
400
|
}
|
|
350
401
|
|
|
351
|
-
fn
|
|
402
|
+
fn shallow_node(handle: &Handle) -> Node {
|
|
352
403
|
let node = handle;
|
|
353
404
|
let mut public_id = String::new();
|
|
354
405
|
let mut system_id = String::new();
|
|
355
406
|
let (node_type, name, value, namespace, attrs) = match &node.data {
|
|
356
|
-
NodeData::Document =>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
407
|
+
NodeData::Document => (
|
|
408
|
+
DOCUMENT_NODE,
|
|
409
|
+
"#document".to_string(),
|
|
410
|
+
String::new(),
|
|
411
|
+
String::new(),
|
|
412
|
+
Vec::new(),
|
|
413
|
+
),
|
|
414
|
+
NodeData::Doctype {
|
|
415
|
+
name,
|
|
416
|
+
public_id: pub_id,
|
|
417
|
+
system_id: sys_id,
|
|
418
|
+
} => {
|
|
360
419
|
public_id = pub_id.to_string();
|
|
361
420
|
system_id = sys_id.to_string();
|
|
362
|
-
(
|
|
421
|
+
(
|
|
422
|
+
DOCUMENT_TYPE_NODE,
|
|
423
|
+
name.to_string(),
|
|
424
|
+
String::new(),
|
|
425
|
+
String::new(),
|
|
426
|
+
Vec::new(),
|
|
427
|
+
)
|
|
363
428
|
}
|
|
364
429
|
NodeData::Text { contents } => (
|
|
365
430
|
TEXT_NODE,
|
|
@@ -389,7 +454,12 @@ fn walk(handle: &Handle) -> Node {
|
|
|
389
454
|
.map(|attr| Attr {
|
|
390
455
|
name: attr.name.local.to_string(),
|
|
391
456
|
value: attr.value.to_string(),
|
|
392
|
-
prefix: attr
|
|
457
|
+
prefix: attr
|
|
458
|
+
.name
|
|
459
|
+
.prefix
|
|
460
|
+
.as_ref()
|
|
461
|
+
.map(std::string::ToString::to_string)
|
|
462
|
+
.unwrap_or_default(),
|
|
393
463
|
})
|
|
394
464
|
.collect();
|
|
395
465
|
// html5lib short namespace form: "" (html), "svg", "math".
|
|
@@ -404,27 +474,67 @@ fn walk(handle: &Handle) -> Node {
|
|
|
404
474
|
}
|
|
405
475
|
};
|
|
406
476
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
477
|
+
Node {
|
|
478
|
+
node_type,
|
|
479
|
+
name,
|
|
480
|
+
value,
|
|
481
|
+
namespace,
|
|
482
|
+
public_id,
|
|
483
|
+
system_id,
|
|
484
|
+
attrs,
|
|
485
|
+
children: Vec::new(),
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
fn walk(handle: &Handle) -> Node {
|
|
490
|
+
enum Task {
|
|
491
|
+
Enter(Handle, bool),
|
|
492
|
+
Finish(Node, usize),
|
|
493
|
+
}
|
|
494
|
+
let mut tasks = vec![Task::Enter(handle.clone(), false)];
|
|
495
|
+
let mut complete = Vec::new();
|
|
496
|
+
while let Some(task) = tasks.pop() {
|
|
497
|
+
match task {
|
|
498
|
+
Task::Enter(handle, content) => {
|
|
499
|
+
let mut node = shallow_node(&handle);
|
|
500
|
+
if content {
|
|
501
|
+
node.node_type = DOCUMENT_FRAGMENT_NODE;
|
|
502
|
+
node.name = "content".into();
|
|
503
|
+
}
|
|
504
|
+
let children = handle.children.borrow();
|
|
505
|
+
let template = match &handle.data {
|
|
506
|
+
NodeData::Element {
|
|
507
|
+
template_contents, ..
|
|
508
|
+
} => template_contents.borrow().clone(),
|
|
509
|
+
_ => None,
|
|
510
|
+
};
|
|
511
|
+
tasks.push(Task::Finish(
|
|
512
|
+
node,
|
|
513
|
+
children.len() + usize::from(template.is_some()),
|
|
514
|
+
));
|
|
515
|
+
if let Some(template) = template {
|
|
516
|
+
tasks.push(Task::Enter(template, true));
|
|
517
|
+
}
|
|
518
|
+
for child in children.iter().rev() {
|
|
519
|
+
tasks.push(Task::Enter(child.clone(), false));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
Task::Finish(mut node, count) => {
|
|
523
|
+
node.children = complete.split_off(complete.len() - count);
|
|
524
|
+
complete.push(node);
|
|
525
|
+
}
|
|
424
526
|
}
|
|
425
527
|
}
|
|
528
|
+
complete.pop().expect("one parsed root")
|
|
529
|
+
}
|
|
426
530
|
|
|
427
|
-
|
|
531
|
+
/// Drain nested parser intermediates without recursive destructor calls.
|
|
532
|
+
/// This leaves the public marshaled Node representation unchanged.
|
|
533
|
+
pub fn discard_node(mut root: Node) {
|
|
534
|
+
let mut pending = std::mem::take(&mut root.children);
|
|
535
|
+
while let Some(mut node) = pending.pop() {
|
|
536
|
+
pending.append(&mut node.children);
|
|
537
|
+
}
|
|
428
538
|
}
|
|
429
539
|
|
|
430
540
|
#[cfg(test)]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|