rusteerio 1.0.1 → 1.1.1

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.
Files changed (57) hide show
  1. package/Cargo.lock +42 -1
  2. package/Cargo.toml +8 -1
  3. package/README.md +90 -51
  4. package/THIRD_PARTY_NOTICES.md +107 -2
  5. package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
  6. package/dist/chunk-FPRP6UIX.mjs.map +1 -0
  7. package/dist/index.cjs +2232 -750
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/slim.cjs +2226 -745
  13. package/dist/slim.cjs.map +1 -1
  14. package/dist/slim.d.mts +2 -2
  15. package/dist/slim.d.ts +2 -2
  16. package/dist/slim.mjs +1 -1
  17. package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
  18. package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
  19. package/dist/utils.d.mts +1 -1
  20. package/dist/utils.d.ts +1 -1
  21. package/native/src/arena.rs +219 -80
  22. package/native/src/attributes.rs +11 -5
  23. package/native/src/document_order.rs +711 -0
  24. package/native/src/extraction.rs +15 -3
  25. package/native/src/gc.rs +12 -3
  26. package/native/src/id_hash.rs +42 -0
  27. package/native/src/immutable.rs +256 -0
  28. package/native/src/lib.rs +1039 -171
  29. package/native/src/runtime.rs +189 -33
  30. package/native/src/selector.rs +346 -44
  31. package/native/src/sink.rs +14 -1
  32. package/native/src/snapshot.rs +527 -0
  33. package/native/src/stream.rs +2 -2
  34. package/native/src/tokenizer.rs +29 -12
  35. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
  36. package/package.json +3 -1
  37. package/rusteerio.darwin-arm64.node +0 -0
  38. package/rusteerio.darwin-x64.node +0 -0
  39. package/rusteerio.linux-arm64-gnu.node +0 -0
  40. package/rusteerio.linux-arm64-musl.node +0 -0
  41. package/rusteerio.linux-x64-gnu.node +0 -0
  42. package/rusteerio.linux-x64-musl.node +0 -0
  43. package/rusteerio.win32-arm64-msvc.node +0 -0
  44. package/rusteerio.win32-x64-msvc.node +0 -0
  45. package/src/attributes.ts +78 -44
  46. package/src/codec.ts +22 -3
  47. package/src/extract.ts +86 -2
  48. package/src/hooks.ts +148 -2
  49. package/src/index.ts +51 -40
  50. package/src/match.ts +207 -0
  51. package/src/native.ts +9 -1
  52. package/src/nodes.ts +531 -183
  53. package/src/router.ts +792 -229
  54. package/src/selection.ts +239 -125
  55. package/src/snapshot.ts +255 -0
  56. package/src/transport.ts +51 -42
  57. package/dist/chunk-DMRD3FGC.mjs.map +0 -1
@@ -64,7 +64,7 @@ fn extract(
64
64
  .unwrap_or(Value::Null));
65
65
  }
66
66
  document.ensure_arena()?;
67
- let mut arena = document.arena.borrow_mut();
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.clone()),
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.clone()),
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");
@@ -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
+ }