rusteerio 1.0.0 → 1.1.0
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 +42 -1
- package/Cargo.toml +8 -1
- package/README.md +90 -51
- package/THIRD_PARTY_NOTICES.md +138 -2
- package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
- package/dist/chunk-FPRP6UIX.mjs.map +1 -0
- package/dist/index.cjs +2232 -750
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs +2226 -745
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.d.mts +2 -2
- package/dist/slim.d.ts +2 -2
- package/dist/slim.mjs +1 -1
- package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
- package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/native/src/arena.rs +231 -602
- package/native/src/attributes.rs +11 -5
- package/native/src/codec.rs +62 -18
- package/native/src/document_order.rs +711 -0
- package/native/src/extraction.rs +15 -3
- package/native/src/gc.rs +12 -3
- package/native/src/hook_sink.rs +77 -3
- package/native/src/id_hash.rs +42 -0
- package/native/src/immutable.rs +256 -0
- package/native/src/lib.rs +1097 -172
- package/native/src/markup_tokenizer.rs +668 -0
- package/native/src/runtime.rs +189 -33
- package/native/src/selector.rs +346 -44
- package/native/src/sink.rs +14 -1
- package/native/src/snapshot.rs +509 -0
- package/native/src/stream.rs +2 -2
- package/native/src/tokenizer.rs +377 -204
- package/native/vendor/README.md +3 -2
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
- package/package.json +3 -1
- package/rusteerio.darwin-arm64.node +0 -0
- package/rusteerio.darwin-x64.node +0 -0
- package/rusteerio.linux-arm64-gnu.node +0 -0
- package/rusteerio.linux-arm64-musl.node +0 -0
- package/rusteerio.linux-x64-gnu.node +0 -0
- package/rusteerio.linux-x64-musl.node +0 -0
- package/rusteerio.win32-arm64-msvc.node +0 -0
- package/rusteerio.win32-x64-msvc.node +0 -0
- package/scripts/generate-notices.mjs +11 -0
- package/scripts/notices/htmlparser2-MIT.txt +18 -0
- package/scripts/notices/sources.json +6 -0
- package/src/attributes.ts +78 -44
- package/src/codec.ts +22 -3
- package/src/extract.ts +86 -2
- package/src/hooks.ts +148 -2
- package/src/index.ts +51 -40
- package/src/match.ts +207 -0
- package/src/native.ts +9 -1
- package/src/nodes.ts +531 -183
- package/src/router.ts +792 -229
- package/src/selection.ts +239 -125
- package/src/snapshot.ts +255 -0
- package/src/transport.ts +51 -42
- package/dist/chunk-DMRD3FGC.mjs.map +0 -1
package/native/src/extraction.rs
CHANGED
|
@@ -64,7 +64,7 @@ fn extract(
|
|
|
64
64
|
.unwrap_or(Value::Null));
|
|
65
65
|
}
|
|
66
66
|
document.ensure_arena()?;
|
|
67
|
-
let mut arena = document.
|
|
67
|
+
let mut arena = document.arena_for_write();
|
|
68
68
|
let arena = arena
|
|
69
69
|
.as_mut()
|
|
70
70
|
.ok_or_else(|| invalid("Missing native document arena"))?;
|
|
@@ -73,7 +73,7 @@ fn extract(
|
|
|
73
73
|
Some(node) if name == "name" => {
|
|
74
74
|
node.name.clone().map(Value::String).unwrap_or(Value::Null)
|
|
75
75
|
}
|
|
76
|
-
Some(node) if name == "type" => Value::String(node.kind.
|
|
76
|
+
Some(node) if name == "type" => Value::String(node.kind.to_string()),
|
|
77
77
|
Some(_) => json!(1),
|
|
78
78
|
None => Value::Null,
|
|
79
79
|
});
|
|
@@ -115,6 +115,18 @@ pub(crate) fn execute(document: &NativeDocument, plan: &Value) -> Result<Value>
|
|
|
115
115
|
extract(document, entries(plan)?, None, 0)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/// Extracts relative to `contexts`, reading the parse tree while it is unchanged.
|
|
119
|
+
pub(crate) fn execute_in(
|
|
120
|
+
document: &NativeDocument,
|
|
121
|
+
plan: &Value,
|
|
122
|
+
contexts: &[u32],
|
|
123
|
+
) -> Result<Value> {
|
|
124
|
+
if plan.get("version").and_then(Value::as_u64) != Some(1) {
|
|
125
|
+
return Err(invalid("Unsupported extraction plan version"));
|
|
126
|
+
}
|
|
127
|
+
extract(document, entries(plan)?, Some(contexts), 0)
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
fn extract_arena(
|
|
119
131
|
arena: &mut crate::arena::DomArena,
|
|
120
132
|
plan: &[Value],
|
|
@@ -152,7 +164,7 @@ fn extract_arena(
|
|
|
152
164
|
Some(node) if name == "name" => {
|
|
153
165
|
node.name.clone().map(Value::String).unwrap_or(Value::Null)
|
|
154
166
|
}
|
|
155
|
-
Some(node) if name == "type" => Value::String(node.kind.
|
|
167
|
+
Some(node) if name == "type" => Value::String(node.kind.to_string()),
|
|
156
168
|
Some(_) => json!(1),
|
|
157
169
|
None => Value::Null,
|
|
158
170
|
});
|
package/native/src/gc.rs
CHANGED
|
@@ -14,16 +14,20 @@ pub(crate) struct GcStats {
|
|
|
14
14
|
|
|
15
15
|
#[cfg(test)]
|
|
16
16
|
pub(crate) fn collect(arena: &mut DomArena, pins: &HashMap<u32, usize>) -> GcStats {
|
|
17
|
-
collect_managed(arena, pins, true)
|
|
17
|
+
collect_managed(arena, pins, true, 0)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/// Reclaims nodes connected to neither a pin, the root (with `keep_root`)
|
|
21
|
+
/// nor a slot below `keep_below`.
|
|
20
22
|
pub(crate) fn collect_managed(
|
|
21
23
|
arena: &mut DomArena,
|
|
22
24
|
pins: &HashMap<u32, usize>,
|
|
23
25
|
keep_root: bool,
|
|
26
|
+
keep_below: u32,
|
|
24
27
|
) -> GcStats {
|
|
25
28
|
let mut marked = vec![false; arena.nodes.len()];
|
|
26
|
-
let mut stack = if keep_root { vec![0] } else { Vec::new() };
|
|
29
|
+
let mut stack: Vec<u32> = if keep_root { vec![0] } else { Vec::new() };
|
|
30
|
+
stack.extend(0..keep_below.min(arena.nodes.len() as u32));
|
|
27
31
|
stack.extend(
|
|
28
32
|
pins.iter()
|
|
29
33
|
.filter_map(|(id, count)| (*count > 0).then_some(*id)),
|
|
@@ -59,6 +63,9 @@ pub(crate) fn collect_managed(
|
|
|
59
63
|
node.attribs = Default::default();
|
|
60
64
|
node.children = Vec::new();
|
|
61
65
|
node.extras = Default::default();
|
|
66
|
+
node.namespace = None;
|
|
67
|
+
node.attribs_namespace = None;
|
|
68
|
+
node.attribs_prefix = None;
|
|
62
69
|
node.parent = None;
|
|
63
70
|
node.prev = None;
|
|
64
71
|
node.next = None;
|
|
@@ -79,6 +86,8 @@ pub(crate) fn collect_managed(
|
|
|
79
86
|
.filter(|node| node.kind == "released")
|
|
80
87
|
.map(|node| node.id)
|
|
81
88
|
.collect::<Vec<_>>();
|
|
89
|
+
arena.detached_since_collection = 0;
|
|
90
|
+
arena.collected_live = arena.nodes.len() as u32 - free.len() as u32;
|
|
82
91
|
GcStats {
|
|
83
92
|
live: arena.nodes.len() as u32 - free.len() as u32,
|
|
84
93
|
slots: arena.nodes.len() as u32,
|
|
@@ -155,7 +164,7 @@ mod tests {
|
|
|
155
164
|
#[test]
|
|
156
165
|
fn managed_detached_handle_does_not_retain_the_original_document() {
|
|
157
166
|
let mut arena = fixture();
|
|
158
|
-
let stats = collect_managed(&mut arena, &HashMap::from([(3, 1)]), false);
|
|
167
|
+
let stats = collect_managed(&mut arena, &HashMap::from([(3, 1)]), false, 0);
|
|
159
168
|
assert_eq!((stats.live, stats.retained, stats.reclaimed), (3, 1, 3));
|
|
160
169
|
assert_eq!(arena.nodes[0].kind, "released");
|
|
161
170
|
assert_eq!(arena.nodes[1].kind, "released");
|
package/native/src/hook_sink.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! An active user-supplied tree adapter. Rust runs the HTML5 algorithm; the
|
|
2
2
|
//! adapter supplies every construction, mutation and tree-inspection decision.
|
|
3
3
|
use std::borrow::Cow;
|
|
4
|
-
use std::cell::{Ref, RefCell};
|
|
4
|
+
use std::cell::{Cell, Ref, RefCell};
|
|
5
5
|
use std::collections::HashMap;
|
|
6
6
|
use std::rc::Rc;
|
|
7
7
|
|
|
@@ -196,6 +196,26 @@ struct HookSink {
|
|
|
196
196
|
context: Option<Rc<crate::diagnostics::DiagnosticContext>>,
|
|
197
197
|
diagnostics: RefCell<Vec<Value>>,
|
|
198
198
|
locations: Option<crate::tracker::Locations<u32>>,
|
|
199
|
+
pending_escape: Cell<Option<TextTarget>>,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Where a text chunk is inserted: appended to `parent`, or before `sibling`.
|
|
203
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
204
|
+
struct TextTarget {
|
|
205
|
+
parent: u32,
|
|
206
|
+
sibling: Option<u32>,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Whether the final character is a codec escape whose character has not
|
|
210
|
+
/// arrived yet. Escapes pair with the following character.
|
|
211
|
+
fn ends_with_unpaired_escape(text: &str) -> bool {
|
|
212
|
+
let mut chars = text.chars();
|
|
213
|
+
while let Some(ch) = chars.next() {
|
|
214
|
+
if ch == crate::codec::ESCAPE && chars.next().is_none() {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
false
|
|
199
219
|
}
|
|
200
220
|
|
|
201
221
|
impl HookSink {
|
|
@@ -203,6 +223,17 @@ impl HookSink {
|
|
|
203
223
|
if self.error.borrow().is_some() {
|
|
204
224
|
return Value::Null;
|
|
205
225
|
}
|
|
226
|
+
if matches!(
|
|
227
|
+
method,
|
|
228
|
+
"appendChild"
|
|
229
|
+
| "insertBefore"
|
|
230
|
+
| "detachNode"
|
|
231
|
+
| "setDocumentType"
|
|
232
|
+
| "setTemplateContent"
|
|
233
|
+
| "__snapshot"
|
|
234
|
+
) {
|
|
235
|
+
self.flush_escape();
|
|
236
|
+
}
|
|
206
237
|
let result = self.bridge.call(method, args);
|
|
207
238
|
match result {
|
|
208
239
|
Ok(value) => value,
|
|
@@ -215,6 +246,38 @@ impl HookSink {
|
|
|
215
246
|
fn node(&self, method: &str, args: Value) -> u32 {
|
|
216
247
|
self.call(method, args).as_u64().unwrap_or(0) as u32
|
|
217
248
|
}
|
|
249
|
+
/// The tokenizer may split a codec escape from its character across text
|
|
250
|
+
/// chunks. Adapters decode each chunk separately, so a trailing escape is
|
|
251
|
+
/// held until the next chunk for the same target.
|
|
252
|
+
fn insert_text(&self, target: TextTarget, text: &str) {
|
|
253
|
+
let mut data = String::with_capacity(text.len() + crate::codec::ESCAPE.len_utf8());
|
|
254
|
+
if let Some(pending) = self.pending_escape.take() {
|
|
255
|
+
if pending == target {
|
|
256
|
+
data.push(crate::codec::ESCAPE);
|
|
257
|
+
} else {
|
|
258
|
+
self.send_text(pending, &crate::codec::ESCAPE.to_string());
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
data.push_str(text);
|
|
262
|
+
if ends_with_unpaired_escape(&data) {
|
|
263
|
+
data.pop();
|
|
264
|
+
self.pending_escape.set(Some(target));
|
|
265
|
+
}
|
|
266
|
+
if !data.is_empty() {
|
|
267
|
+
self.send_text(target, &data);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
fn send_text(&self, target: TextTarget, data: &str) {
|
|
271
|
+
match target.sibling {
|
|
272
|
+
None => self.call("insertText", json!([target.parent, data])),
|
|
273
|
+
Some(sibling) => self.call("insertTextBefore", json!([target.parent, data, sibling])),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
fn flush_escape(&self) {
|
|
277
|
+
if let Some(pending) = self.pending_escape.take() {
|
|
278
|
+
self.send_text(pending, &crate::codec::ESCAPE.to_string());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
218
281
|
fn attrs(attrs: Vec<Attribute>) -> Value {
|
|
219
282
|
Value::Array(
|
|
220
283
|
attrs
|
|
@@ -463,7 +526,11 @@ impl TreeSink for HookSink {
|
|
|
463
526
|
}
|
|
464
527
|
}
|
|
465
528
|
NodeOrText::AppendText(text) => {
|
|
466
|
-
|
|
529
|
+
let target = TextTarget {
|
|
530
|
+
parent: *parent,
|
|
531
|
+
sibling: None,
|
|
532
|
+
};
|
|
533
|
+
self.insert_text(target, text.as_ref());
|
|
467
534
|
self.mark_text(*parent, None, text.as_ref());
|
|
468
535
|
}
|
|
469
536
|
}
|
|
@@ -530,7 +597,11 @@ impl TreeSink for HookSink {
|
|
|
530
597
|
self.call("insertBefore", json!([parent, node, sibling]));
|
|
531
598
|
}
|
|
532
599
|
NodeOrText::AppendText(text) => {
|
|
533
|
-
|
|
600
|
+
let target = TextTarget {
|
|
601
|
+
parent,
|
|
602
|
+
sibling: Some(*sibling),
|
|
603
|
+
};
|
|
604
|
+
self.insert_text(target, text.as_ref());
|
|
534
605
|
self.mark_text(parent, Some(*sibling), text.as_ref());
|
|
535
606
|
}
|
|
536
607
|
}
|
|
@@ -589,6 +660,7 @@ fn parser(
|
|
|
589
660
|
context: None,
|
|
590
661
|
diagnostics: RefCell::new(Vec::new()),
|
|
591
662
|
locations: None,
|
|
663
|
+
pending_escape: Cell::new(None),
|
|
592
664
|
};
|
|
593
665
|
sink.call(
|
|
594
666
|
if is_document {
|
|
@@ -659,6 +731,7 @@ pub(crate) fn parse(
|
|
|
659
731
|
diagnostics: RefCell::new(Vec::new()),
|
|
660
732
|
locations: (options.get("sourceCodeLocationInfo") == Some(&Value::Bool(true)))
|
|
661
733
|
.then(|| crate::tracker::Locations::new(context.clone())),
|
|
734
|
+
pending_escape: Cell::new(None),
|
|
662
735
|
};
|
|
663
736
|
sink.call(
|
|
664
737
|
if is_document {
|
|
@@ -706,6 +779,7 @@ where
|
|
|
706
779
|
diagnostics: RefCell::new(Vec::new()),
|
|
707
780
|
locations: (options.get("sourceCodeLocationInfo") == Some(&Value::Bool(true)))
|
|
708
781
|
.then(|| crate::tracker::Locations::new(context.clone())),
|
|
782
|
+
pending_escape: Cell::new(None),
|
|
709
783
|
};
|
|
710
784
|
sink.call(
|
|
711
785
|
if is_document {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//! A fast hasher for maps keyed by node ids. The default SipHash resists
|
|
2
|
+
//! adversarial keys, which tree-assigned ids cannot be, at several times the cost.
|
|
3
|
+
use std::collections::{HashMap, HashSet};
|
|
4
|
+
use std::hash::{BuildHasherDefault, Hasher};
|
|
5
|
+
|
|
6
|
+
const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
|
|
7
|
+
|
|
8
|
+
#[derive(Default, Clone, Copy)]
|
|
9
|
+
pub(crate) struct IdHasher(u64);
|
|
10
|
+
|
|
11
|
+
impl IdHasher {
|
|
12
|
+
fn add(&mut self, value: u64) {
|
|
13
|
+
self.0 = (self.0.rotate_left(5) ^ value).wrapping_mul(SEED);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl Hasher for IdHasher {
|
|
18
|
+
fn finish(&self) -> u64 {
|
|
19
|
+
self.0
|
|
20
|
+
}
|
|
21
|
+
fn write(&mut self, bytes: &[u8]) {
|
|
22
|
+
let (chunks, remainder) = bytes.as_chunks::<8>();
|
|
23
|
+
for chunk in chunks {
|
|
24
|
+
self.add(u64::from_le_bytes(*chunk));
|
|
25
|
+
}
|
|
26
|
+
for byte in remainder {
|
|
27
|
+
self.add(u64::from(*byte));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
fn write_u32(&mut self, value: u32) {
|
|
31
|
+
self.add(u64::from(value));
|
|
32
|
+
}
|
|
33
|
+
fn write_u64(&mut self, value: u64) {
|
|
34
|
+
self.add(value);
|
|
35
|
+
}
|
|
36
|
+
fn write_usize(&mut self, value: usize) {
|
|
37
|
+
self.add(value as u64);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub(crate) type IdMap<K, V> = HashMap<K, V, BuildHasherDefault<IdHasher>>;
|
|
42
|
+
pub(crate) type IdSet<K> = HashSet<K, BuildHasherDefault<IdHasher>>;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
//! Read-only selection over a parsed document. Queries, matching and ordering
|
|
2
|
+
//! run on the parse tree, so a document is copied into the mutable arena only
|
|
3
|
+
//! when it is first changed.
|
|
4
|
+
use ego_tree::iter::Edge;
|
|
5
|
+
use ego_tree::NodeId;
|
|
6
|
+
use scraper::{Html, Node};
|
|
7
|
+
use serde_json::Value;
|
|
8
|
+
|
|
9
|
+
use crate::selector::SelectDom;
|
|
10
|
+
|
|
11
|
+
const NONE: u32 = u32::MAX;
|
|
12
|
+
|
|
13
|
+
/// Parent and child links by local id. Local ids are preorder positions, so
|
|
14
|
+
/// ascending order is document order.
|
|
15
|
+
pub(crate) struct TreeIndex {
|
|
16
|
+
parent: Vec<u32>,
|
|
17
|
+
child_start: Vec<u32>,
|
|
18
|
+
child_end: Vec<u32>,
|
|
19
|
+
children: Vec<u32>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl TreeIndex {
|
|
23
|
+
pub(crate) fn build(html: &Html, count: usize) -> Self {
|
|
24
|
+
let mut parent = vec![NONE; count];
|
|
25
|
+
let mut stack: Vec<u32> = Vec::new();
|
|
26
|
+
let mut next = 0u32;
|
|
27
|
+
for edge in html.tree.root().traverse() {
|
|
28
|
+
match edge {
|
|
29
|
+
Edge::Open(_) => {
|
|
30
|
+
if (next as usize) < count {
|
|
31
|
+
parent[next as usize] = stack.last().copied().unwrap_or(NONE);
|
|
32
|
+
}
|
|
33
|
+
stack.push(next);
|
|
34
|
+
next += 1;
|
|
35
|
+
}
|
|
36
|
+
Edge::Close(_) => {
|
|
37
|
+
stack.pop();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let mut counts = vec![0u32; count];
|
|
42
|
+
for parent in &parent {
|
|
43
|
+
if let Some(slot) = counts.get_mut(*parent as usize) {
|
|
44
|
+
*slot += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
let mut total = 0u32;
|
|
48
|
+
let child_start: Vec<u32> = counts
|
|
49
|
+
.iter()
|
|
50
|
+
.map(|count| {
|
|
51
|
+
let start = total;
|
|
52
|
+
total += count;
|
|
53
|
+
start
|
|
54
|
+
})
|
|
55
|
+
.collect();
|
|
56
|
+
let mut child_end = child_start.clone();
|
|
57
|
+
let mut children = vec![0u32; total as usize];
|
|
58
|
+
for (local, parent) in parent.iter().enumerate() {
|
|
59
|
+
if let Some(end) = child_end.get_mut(*parent as usize) {
|
|
60
|
+
children[*end as usize] = local as u32;
|
|
61
|
+
*end += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
Self {
|
|
65
|
+
parent,
|
|
66
|
+
child_start,
|
|
67
|
+
child_end,
|
|
68
|
+
children,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// The index of a tree some of whose nodes were detached after parsing,
|
|
73
|
+
/// when traversal order no longer matches local ids.
|
|
74
|
+
pub(crate) fn build_mapped(
|
|
75
|
+
html: &Html,
|
|
76
|
+
ids: &[NodeId],
|
|
77
|
+
reverse: &crate::id_hash::IdMap<NodeId, u32>,
|
|
78
|
+
) -> Self {
|
|
79
|
+
let local = |node: Option<ego_tree::NodeRef<'_, Node>>| {
|
|
80
|
+
node.and_then(|node| reverse.get(&node.id()).copied())
|
|
81
|
+
};
|
|
82
|
+
let mut parent = vec![NONE; ids.len()];
|
|
83
|
+
let mut child_start = vec![0u32; ids.len()];
|
|
84
|
+
let mut child_end = vec![0u32; ids.len()];
|
|
85
|
+
let mut children = Vec::with_capacity(ids.len());
|
|
86
|
+
for (index, id) in ids.iter().enumerate() {
|
|
87
|
+
let Some(node) = html.tree.get(*id) else {
|
|
88
|
+
continue;
|
|
89
|
+
};
|
|
90
|
+
parent[index] = local(node.parent()).unwrap_or(NONE);
|
|
91
|
+
child_start[index] = children.len() as u32;
|
|
92
|
+
children.extend(node.children().filter_map(|child| local(Some(child))));
|
|
93
|
+
child_end[index] = children.len() as u32;
|
|
94
|
+
}
|
|
95
|
+
Self {
|
|
96
|
+
parent,
|
|
97
|
+
child_start,
|
|
98
|
+
child_end,
|
|
99
|
+
children,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub(crate) fn parent(&self, id: u32) -> Option<u32> {
|
|
104
|
+
self.parent
|
|
105
|
+
.get(id as usize)
|
|
106
|
+
.copied()
|
|
107
|
+
.filter(|parent| *parent != NONE)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pub(crate) fn children(&self, id: u32) -> &[u32] {
|
|
111
|
+
match (
|
|
112
|
+
self.child_start.get(id as usize),
|
|
113
|
+
self.child_end.get(id as usize),
|
|
114
|
+
) {
|
|
115
|
+
(Some(start), Some(end)) => &self.children[*start as usize..*end as usize],
|
|
116
|
+
_ => &[],
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Whether `ancestor` is a proper ancestor of `id`.
|
|
121
|
+
pub(crate) fn contains(&self, ancestor: u32, id: u32) -> bool {
|
|
122
|
+
let mut current = self.parent(id);
|
|
123
|
+
while let Some(node) = current {
|
|
124
|
+
if node == ancestor {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
current = self.parent(node);
|
|
128
|
+
}
|
|
129
|
+
false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// The parse tree seen through the same interface as the mutable arena.
|
|
134
|
+
pub(crate) struct TreeDom<'a> {
|
|
135
|
+
pub html: &'a Html,
|
|
136
|
+
pub ids: &'a [NodeId],
|
|
137
|
+
pub index: &'a TreeIndex,
|
|
138
|
+
pub options: &'a Value,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
impl TreeDom<'_> {
|
|
142
|
+
fn node(&self, id: u32) -> Option<ego_tree::NodeRef<'_, Node>> {
|
|
143
|
+
self.html.tree.get(*self.ids.get(id as usize)?)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The value the arena would hold: attributes are keyed by local name and
|
|
147
|
+
/// a repeated local name keeps its last value.
|
|
148
|
+
pub(crate) fn element_attr(&self, id: u32, name: &str) -> Option<&str> {
|
|
149
|
+
match self.node(id)?.value() {
|
|
150
|
+
Node::Element(element) => element
|
|
151
|
+
.attrs
|
|
152
|
+
.iter()
|
|
153
|
+
.rev()
|
|
154
|
+
.find(|(key, _)| key.local.as_ref() == name)
|
|
155
|
+
.map(|(_, value)| value.as_ref()),
|
|
156
|
+
_ => None,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub(crate) fn is_element(&self, id: u32) -> bool {
|
|
161
|
+
matches!(
|
|
162
|
+
self.node(id).map(|node| node.value()),
|
|
163
|
+
Some(Node::Element(_))
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
impl SelectDom for TreeDom<'_> {
|
|
169
|
+
fn kind(&self, id: u32) -> &str {
|
|
170
|
+
match self.node(id).map(|node| node.value()) {
|
|
171
|
+
Some(Node::Element(element)) => match element.name() {
|
|
172
|
+
"script" => "script",
|
|
173
|
+
"style" => "style",
|
|
174
|
+
_ => "tag",
|
|
175
|
+
},
|
|
176
|
+
Some(Node::Text(_)) => "text",
|
|
177
|
+
Some(Node::Comment(_)) => "comment",
|
|
178
|
+
Some(Node::Doctype(_) | Node::ProcessingInstruction(_)) => "directive",
|
|
179
|
+
Some(Node::Document | Node::Fragment) => "root",
|
|
180
|
+
None => "",
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
fn name(&self, id: u32) -> Option<&str> {
|
|
184
|
+
match self.node(id)?.value() {
|
|
185
|
+
Node::Element(element) => Some(element.name()),
|
|
186
|
+
Node::Doctype(_) => Some("!doctype"),
|
|
187
|
+
Node::ProcessingInstruction(instruction) => Some(&instruction.target),
|
|
188
|
+
_ => None,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
fn attr(&self, id: u32, name: &str) -> Option<&str> {
|
|
192
|
+
self.element_attr(id, name)
|
|
193
|
+
}
|
|
194
|
+
fn parent(&self, id: u32) -> Option<u32> {
|
|
195
|
+
self.index.parent(id)
|
|
196
|
+
}
|
|
197
|
+
fn children(&self, id: u32) -> &[u32] {
|
|
198
|
+
self.index.children(id)
|
|
199
|
+
}
|
|
200
|
+
fn text(&self, id: u32) -> String {
|
|
201
|
+
let mut output = String::new();
|
|
202
|
+
let mut stack = vec![id];
|
|
203
|
+
while let Some(id) = stack.pop() {
|
|
204
|
+
match self.node(id).map(|node| node.value()) {
|
|
205
|
+
Some(Node::Text(text)) => output.push_str(text),
|
|
206
|
+
Some(_) => stack.extend(self.index.children(id).iter().rev().copied()),
|
|
207
|
+
None => {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
output
|
|
211
|
+
}
|
|
212
|
+
fn xml_mode(&self) -> bool {
|
|
213
|
+
crate::arena::xml_mode(self.options)
|
|
214
|
+
}
|
|
215
|
+
fn lower_case_tags(&self) -> bool {
|
|
216
|
+
self.options
|
|
217
|
+
.get("lowerCaseTags")
|
|
218
|
+
.and_then(Value::as_bool)
|
|
219
|
+
.unwrap_or(!self.xml_mode())
|
|
220
|
+
}
|
|
221
|
+
fn lower_case_attribute_names(&self) -> bool {
|
|
222
|
+
self.options
|
|
223
|
+
.get("lowerCaseAttributeNames")
|
|
224
|
+
.and_then(Value::as_bool)
|
|
225
|
+
.unwrap_or(!self.xml_mode())
|
|
226
|
+
}
|
|
227
|
+
fn custom_pseudo(&self, id: u32, name: &str, arg: Option<&str>) -> Option<bool> {
|
|
228
|
+
let key = format!("{}({})", name, arg.unwrap_or(""));
|
|
229
|
+
self.options
|
|
230
|
+
.get("__pseudoMatches")?
|
|
231
|
+
.get(&key)?
|
|
232
|
+
.as_array()
|
|
233
|
+
.map(|ids| ids.iter().any(|value| value.as_u64() == Some(id as u64)))
|
|
234
|
+
}
|
|
235
|
+
fn pseudo_alias(&self, name: &str) -> Option<&str> {
|
|
236
|
+
self.options.get("pseudos")?.get(name)?.as_str()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Whether a class attribute contains `needle` as a whitespace-separated token,
|
|
241
|
+
/// exactly as the arena's `hasClass` reads it.
|
|
242
|
+
pub(crate) fn has_class_token(classes: &str, needle: &str) -> bool {
|
|
243
|
+
!needle.is_empty()
|
|
244
|
+
&& classes.match_indices(needle).any(|(index, _)| {
|
|
245
|
+
(index == 0
|
|
246
|
+
|| classes[..index]
|
|
247
|
+
.chars()
|
|
248
|
+
.next_back()
|
|
249
|
+
.is_some_and(char::is_whitespace))
|
|
250
|
+
&& (index + needle.len() == classes.len()
|
|
251
|
+
|| classes[index + needle.len()..]
|
|
252
|
+
.chars()
|
|
253
|
+
.next()
|
|
254
|
+
.is_some_and(char::is_whitespace))
|
|
255
|
+
})
|
|
256
|
+
}
|