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.
Files changed (65) 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 +138 -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 +231 -602
  22. package/native/src/attributes.rs +11 -5
  23. package/native/src/codec.rs +62 -18
  24. package/native/src/document_order.rs +711 -0
  25. package/native/src/extraction.rs +15 -3
  26. package/native/src/gc.rs +12 -3
  27. package/native/src/hook_sink.rs +77 -3
  28. package/native/src/id_hash.rs +42 -0
  29. package/native/src/immutable.rs +256 -0
  30. package/native/src/lib.rs +1097 -172
  31. package/native/src/markup_tokenizer.rs +668 -0
  32. package/native/src/runtime.rs +189 -33
  33. package/native/src/selector.rs +346 -44
  34. package/native/src/sink.rs +14 -1
  35. package/native/src/snapshot.rs +509 -0
  36. package/native/src/stream.rs +2 -2
  37. package/native/src/tokenizer.rs +377 -204
  38. package/native/vendor/README.md +3 -2
  39. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
  40. package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
  41. package/package.json +3 -1
  42. package/rusteerio.darwin-arm64.node +0 -0
  43. package/rusteerio.darwin-x64.node +0 -0
  44. package/rusteerio.linux-arm64-gnu.node +0 -0
  45. package/rusteerio.linux-arm64-musl.node +0 -0
  46. package/rusteerio.linux-x64-gnu.node +0 -0
  47. package/rusteerio.linux-x64-musl.node +0 -0
  48. package/rusteerio.win32-arm64-msvc.node +0 -0
  49. package/rusteerio.win32-x64-msvc.node +0 -0
  50. package/scripts/generate-notices.mjs +11 -0
  51. package/scripts/notices/htmlparser2-MIT.txt +18 -0
  52. package/scripts/notices/sources.json +6 -0
  53. package/src/attributes.ts +78 -44
  54. package/src/codec.ts +22 -3
  55. package/src/extract.ts +86 -2
  56. package/src/hooks.ts +148 -2
  57. package/src/index.ts +51 -40
  58. package/src/match.ts +207 -0
  59. package/src/native.ts +9 -1
  60. package/src/nodes.ts +531 -183
  61. package/src/router.ts +792 -229
  62. package/src/selection.ts +239 -125
  63. package/src/snapshot.ts +255 -0
  64. package/src/transport.ts +51 -42
  65. package/dist/chunk-DMRD3FGC.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  //! Cheerio-compatible CSS and jQuery selection over the native mutable DOM.
2
2
 
3
+ use crate::id_hash::{IdMap, IdSet};
3
4
  use napi::{Error, Result, Status};
4
- use std::collections::HashSet;
5
5
 
6
6
  pub(crate) trait SelectDom {
7
7
  fn kind(&self, id: u32) -> &str;
@@ -23,8 +23,68 @@ pub(crate) trait SelectDom {
23
23
  fn pseudo_alias(&self, _name: &str) -> Option<&str> {
24
24
  None
25
25
  }
26
+ /// Every element whose ASCII-lowercased name is `lowercase_name`, in
27
+ /// document order, when the tree keeps such an index.
28
+ fn elements_named(&self, _lowercase_name: &str) -> Option<&[u32]> {
29
+ None
30
+ }
31
+ /// Whether ascending ids are document order for every node.
32
+ fn is_id_order_document_order(&self) -> bool {
33
+ false
34
+ }
26
35
  }
27
36
 
37
+ /// Attributes whose values css-select compares case-insensitively in HTML
38
+ /// documents when a selector has no `i` or `s` flag.
39
+ pub(crate) const CASE_INSENSITIVE_ATTRIBUTES: &[&str] = &[
40
+ "accept",
41
+ "accept-charset",
42
+ "align",
43
+ "alink",
44
+ "axis",
45
+ "bgcolor",
46
+ "charset",
47
+ "checked",
48
+ "clear",
49
+ "codetype",
50
+ "color",
51
+ "compact",
52
+ "declare",
53
+ "defer",
54
+ "dir",
55
+ "direction",
56
+ "disabled",
57
+ "enctype",
58
+ "face",
59
+ "frame",
60
+ "hreflang",
61
+ "http-equiv",
62
+ "lang",
63
+ "language",
64
+ "link",
65
+ "media",
66
+ "method",
67
+ "multiple",
68
+ "nohref",
69
+ "noresize",
70
+ "noshade",
71
+ "nowrap",
72
+ "readonly",
73
+ "rel",
74
+ "rev",
75
+ "rules",
76
+ "scope",
77
+ "scrolling",
78
+ "selected",
79
+ "shape",
80
+ "target",
81
+ "text",
82
+ "type",
83
+ "valign",
84
+ "valuetype",
85
+ "vlink",
86
+ ];
87
+
28
88
  const KNOWN: &[&str] = &[
29
89
  "not",
30
90
  "is",
@@ -119,8 +179,8 @@ fn validate<D: SelectDom>(dom: &D, complex: &Complex) -> Result<()> {
119
179
  return Err(invalid(format!("Unknown pseudo-class :{name}")));
120
180
  }
121
181
  if matches!(name.as_str(), "not" | "is" | "where" | "matches" | "has") {
122
- for nested in parse(argument)? {
123
- validate(dom, &nested)?;
182
+ for nested in parse(argument)?.iter() {
183
+ validate(dom, nested)?;
124
184
  }
125
185
  }
126
186
  }
@@ -204,7 +264,8 @@ fn unescape(value: &str) -> String {
204
264
 
205
265
  fn equal_strings(left: &str, right: &str) -> bool {
206
266
  left == right
207
- || (crate::codec::has_encoding(left) || crate::codec::has_encoding(right))
267
+ || !(left.is_ascii() && right.is_ascii())
268
+ && (crate::codec::has_encoding(left) || crate::codec::has_encoding(right))
208
269
  && crate::codec::to_units(left) == crate::codec::to_units(right)
209
270
  }
210
271
 
@@ -489,7 +550,29 @@ fn parse_compound(source: &str) -> Result<Vec<Test>> {
489
550
  Ok(tests)
490
551
  }
491
552
 
492
- fn parse(source: &str) -> Result<Vec<Complex>> {
553
+ thread_local! {
554
+ /// Parsed selectors by source; queries reuse a handful of selectors many times.
555
+ static PARSED: std::cell::RefCell<std::collections::HashMap<String, std::rc::Rc<Vec<Complex>>>> =
556
+ std::cell::RefCell::new(std::collections::HashMap::new());
557
+ }
558
+ const PARSED_LIMIT: usize = 512;
559
+
560
+ fn parse(source: &str) -> Result<std::rc::Rc<Vec<Complex>>> {
561
+ if let Some(parsed) = PARSED.with(|cache| cache.borrow().get(source).cloned()) {
562
+ return Ok(parsed);
563
+ }
564
+ let parsed = std::rc::Rc::new(parse_uncached(source)?);
565
+ PARSED.with(|cache| {
566
+ let mut cache = cache.borrow_mut();
567
+ if cache.len() >= PARSED_LIMIT {
568
+ cache.clear();
569
+ }
570
+ cache.insert(source.to_owned(), parsed.clone());
571
+ });
572
+ Ok(parsed)
573
+ }
574
+
575
+ fn parse_uncached(source: &str) -> Result<Vec<Complex>> {
493
576
  split_groups(source)?
494
577
  .iter()
495
578
  .map(|group| {
@@ -568,7 +651,7 @@ fn parse(source: &str) -> Result<Vec<Complex>> {
568
651
  }
569
652
 
570
653
  fn unique(ids: impl IntoIterator<Item = u32>) -> Vec<u32> {
571
- let mut seen = HashSet::new();
654
+ let mut seen = IdSet::default();
572
655
  ids.into_iter().filter(|id| seen.insert(*id)).collect()
573
656
  }
574
657
 
@@ -587,7 +670,29 @@ fn descendants<D: SelectDom>(dom: &D, roots: &[u32]) -> Vec<u32> {
587
670
  stack.extend(dom.children(id).iter().rev().copied());
588
671
  }
589
672
  }
590
- unique(output)
673
+ // One root's descendants are distinct; only overlapping roots repeat.
674
+ if roots.len() > 1 {
675
+ unique(output)
676
+ } else {
677
+ output
678
+ }
679
+ }
680
+
681
+ /// Visits the elements below `root` in document order, as `descendants` lists
682
+ /// them for one root, until `visit` returns false.
683
+ fn for_each_descendant<D: SelectDom>(dom: &D, root: u32, mut visit: impl FnMut(u32) -> bool) {
684
+ let scope_barrier = tag(dom, root);
685
+ let mut stack: Vec<u32> = dom.children(root).iter().rev().copied().collect();
686
+ while let Some(id) = stack.pop() {
687
+ let kind = dom.kind(id);
688
+ if scope_barrier && kind == "root" {
689
+ continue;
690
+ }
691
+ if matches!(kind, "tag" | "script" | "style") && !visit(id) {
692
+ return;
693
+ }
694
+ stack.extend(dom.children(id).iter().rev().copied());
695
+ }
591
696
  }
592
697
 
593
698
  fn siblings<D: SelectDom>(dom: &D, id: u32) -> Vec<u32> {
@@ -637,13 +742,13 @@ fn position(name: &str) -> bool {
637
742
  }
638
743
 
639
744
  fn has_position(source: &str) -> Result<bool> {
640
- for complex in parse(source)? {
641
- for part in complex.parts {
745
+ for complex in parse(source)?.iter() {
746
+ for part in &complex.parts {
642
747
  for test in part {
643
748
  if let Test::Pseudo(name, argument) = test {
644
- if position(&name)
749
+ if position(name)
645
750
  || (matches!(name.as_str(), "not" | "is" | "where" | "matches")
646
- && has_position(&argument)?)
751
+ && has_position(argument)?)
647
752
  {
648
753
  return Ok(true);
649
754
  }
@@ -775,22 +880,7 @@ fn attr_matches<D: SelectDom>(
775
880
  let insensitive = flag == Some('i')
776
881
  || (flag != Some('s')
777
882
  && !dom.xml_mode()
778
- && matches!(
779
- name.as_str(),
780
- "type"
781
- | "lang"
782
- | "dir"
783
- | "rel"
784
- | "target"
785
- | "method"
786
- | "enctype"
787
- | "align"
788
- | "valign"
789
- | "charset"
790
- | "hreflang"
791
- | "media"
792
- | "http-equiv"
793
- ));
883
+ && CASE_INSENSITIVE_ATTRIBUTES.contains(&name.as_str()));
794
884
  let (attribute, value) = if insensitive {
795
885
  (attribute.to_lowercase(), value.to_lowercase())
796
886
  } else {
@@ -819,6 +909,49 @@ fn matches_backwards<D: SelectDom>(
819
909
  complex: &Complex,
820
910
  index: usize,
821
911
  scope: &[u32],
912
+ memo: &mut BackwardMemo,
913
+ ) -> Result<bool> {
914
+ matches_backwards_within(dom, id, complex, index, scope, None, memo)
915
+ }
916
+
917
+ /// Results of matching an element against the compounds up to an index, for
918
+ /// one complex selector, scope and allowed set. Without it, chains of
919
+ /// descendant combinators retry the same ancestors for every candidate,
920
+ /// which grows exponentially with nesting depth.
921
+ #[derive(Default)]
922
+ struct BackwardMemo(IdMap<u64, bool>);
923
+
924
+ /// Matches right to left. With `allowed`, every element matched for an
925
+ /// earlier compound must be in that set.
926
+ fn matches_backwards_within<D: SelectDom>(
927
+ dom: &D,
928
+ id: u32,
929
+ complex: &Complex,
930
+ index: usize,
931
+ scope: &[u32],
932
+ allowed: Option<&IdSet<u32>>,
933
+ memo: &mut BackwardMemo,
934
+ ) -> Result<bool> {
935
+ // One combinator cannot repeat work; the memo would only cost lookups.
936
+ let key = (complex.parts.len() > 2).then(|| (index as u64) << 32 | u64::from(id));
937
+ if let Some(known) = key.and_then(|key| memo.0.get(&key)) {
938
+ return Ok(*known);
939
+ }
940
+ let matched = matches_backwards_uncached(dom, id, complex, index, scope, allowed, memo)?;
941
+ if let Some(key) = key {
942
+ memo.0.insert(key, matched);
943
+ }
944
+ Ok(matched)
945
+ }
946
+
947
+ fn matches_backwards_uncached<D: SelectDom>(
948
+ dom: &D,
949
+ id: u32,
950
+ complex: &Complex,
951
+ index: usize,
952
+ scope: &[u32],
953
+ allowed: Option<&IdSet<u32>>,
954
+ memo: &mut BackwardMemo,
822
955
  ) -> Result<bool> {
823
956
  if !matches_compound(dom, id, &complex.parts[index], scope)? {
824
957
  return Ok(false);
@@ -857,16 +990,43 @@ fn matches_backwards<D: SelectDom>(
857
990
  }
858
991
  };
859
992
  for candidate in candidates {
860
- if matches_backwards(dom, candidate, complex, index - 1, scope)? {
993
+ if allowed.is_some_and(|allowed| !allowed.contains(&candidate)) {
994
+ continue;
995
+ }
996
+ if matches_backwards_within(dom, candidate, complex, index - 1, scope, allowed, memo)? {
861
997
  return Ok(true);
862
998
  }
863
999
  }
864
1000
  Ok(false)
865
1001
  }
866
1002
 
1003
+ /// Whether evaluating `complex` right to left gives the same result as
1004
+ /// expanding it left to right: no leading combinator, no `:scope`, and no
1005
+ /// positional pseudo-class, which filters each intermediate selection.
1006
+ fn is_backward_safe(complex: &Complex) -> Result<bool> {
1007
+ if complex.leading.is_some() || complex.parts.len() < 2 {
1008
+ return Ok(false);
1009
+ }
1010
+ for part in &complex.parts {
1011
+ for test in part {
1012
+ if let Test::Pseudo(name, argument) = test {
1013
+ if name == "scope"
1014
+ || position(name)
1015
+ || (matches!(name.as_str(), "not" | "is" | "where" | "matches")
1016
+ && has_position(argument)?)
1017
+ {
1018
+ return Ok(false);
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ Ok(true)
1024
+ }
1025
+
867
1026
  fn matches_expression<D: SelectDom>(dom: &D, id: u32, source: &str, scope: &[u32]) -> Result<bool> {
868
- for complex in parse(source)? {
869
- if matches_backwards(dom, id, &complex, complex.parts.len() - 1, scope)? {
1027
+ for complex in parse(source)?.iter() {
1028
+ let mut memo = BackwardMemo::default();
1029
+ if matches_backwards(dom, id, complex, complex.parts.len() - 1, scope, &mut memo)? {
870
1030
  return Ok(true);
871
1031
  }
872
1032
  }
@@ -890,7 +1050,7 @@ fn pseudo<D: SelectDom>(
890
1050
  Ok(match name {
891
1051
  "not" => !matches_expression(dom, id, argument, scope)?,
892
1052
  "is" | "where" | "matches" => matches_expression(dom, id, argument, scope)?,
893
- "has" => !select(dom, argument, Some(&[id]))?.is_empty(),
1053
+ "has" => !select_in(dom, argument, Some(&[id]))?.is_empty(),
894
1054
  "contains" | "icontains" => {
895
1055
  let value = unescape(argument.trim_matches(['\'', '"']));
896
1056
  let text = dom.text(id);
@@ -923,7 +1083,7 @@ fn pseudo<D: SelectDom>(
923
1083
  let mut argument = argument;
924
1084
  if let Some((formula, of)) = argument.split_once(" of ") {
925
1085
  argument = formula;
926
- children = filter(dom, of, &children)?;
1086
+ children = filter_in(dom, of, &children)?;
927
1087
  }
928
1088
  let Some(index) = children.iter().position(|child| *child == id) else {
929
1089
  return Ok(false);
@@ -1004,6 +1164,15 @@ fn pseudo<D: SelectDom>(
1004
1164
  })
1005
1165
  }
1006
1166
 
1167
+ /// The candidates of a compound that names an element type, from the tree's
1168
+ /// name index; `matches_compound` still tests each one.
1169
+ fn elements_named_by<'a, D: SelectDom>(dom: &'a D, tests: &[Test]) -> Option<&'a [u32]> {
1170
+ tests.iter().find_map(|test| match test {
1171
+ Test::Tag(name) => dom.elements_named(&name.to_ascii_lowercase()),
1172
+ _ => None,
1173
+ })
1174
+ }
1175
+
1007
1176
  fn matches_compound<D: SelectDom>(dom: &D, id: u32, tests: &[Test], scope: &[u32]) -> Result<bool> {
1008
1177
  if !tag(dom, id) {
1009
1178
  return Ok(false);
@@ -1088,7 +1257,7 @@ fn apply<D: SelectDom>(dom: &D, ids: Vec<u32>, tests: &[Test], scope: &[u32]) ->
1088
1257
  } else if matches!(name.as_str(), "not" | "is" | "where" | "matches")
1089
1258
  && has_position(argument)?
1090
1259
  {
1091
- let wanted: HashSet<_> = filter(dom, argument, &result)?.into_iter().collect();
1260
+ let wanted: IdSet<_> = filter_in(dom, argument, &result)?.into_iter().collect();
1092
1261
  result.retain(|id| wanted.contains(id) != (name == "not"));
1093
1262
  }
1094
1263
  }
@@ -1097,11 +1266,81 @@ fn apply<D: SelectDom>(dom: &D, ids: Vec<u32>, tests: &[Test], scope: &[u32]) ->
1097
1266
  }
1098
1267
 
1099
1268
  /// Query descendants of contexts (the root document when omitted).
1269
+ /// Option-derived flags, read once per query rather than for every node tested.
1270
+ struct Flagged<'a, D> {
1271
+ dom: &'a D,
1272
+ xml: bool,
1273
+ lower_tags: bool,
1274
+ lower_attributes: bool,
1275
+ }
1276
+
1277
+ impl<'a, D: SelectDom> Flagged<'a, D> {
1278
+ fn new(dom: &'a D) -> Self {
1279
+ Self {
1280
+ dom,
1281
+ xml: dom.xml_mode(),
1282
+ lower_tags: dom.lower_case_tags(),
1283
+ lower_attributes: dom.lower_case_attribute_names(),
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ impl<D: SelectDom> SelectDom for Flagged<'_, D> {
1289
+ fn kind(&self, id: u32) -> &str {
1290
+ self.dom.kind(id)
1291
+ }
1292
+ fn name(&self, id: u32) -> Option<&str> {
1293
+ self.dom.name(id)
1294
+ }
1295
+ fn attr(&self, id: u32, name: &str) -> Option<&str> {
1296
+ self.dom.attr(id, name)
1297
+ }
1298
+ fn parent(&self, id: u32) -> Option<u32> {
1299
+ self.dom.parent(id)
1300
+ }
1301
+ fn children(&self, id: u32) -> &[u32] {
1302
+ self.dom.children(id)
1303
+ }
1304
+ fn text(&self, id: u32) -> String {
1305
+ self.dom.text(id)
1306
+ }
1307
+ fn xml_mode(&self) -> bool {
1308
+ self.xml
1309
+ }
1310
+ fn lower_case_tags(&self) -> bool {
1311
+ self.lower_tags
1312
+ }
1313
+ fn lower_case_attribute_names(&self) -> bool {
1314
+ self.lower_attributes
1315
+ }
1316
+ fn custom_pseudo(&self, id: u32, name: &str, argument: Option<&str>) -> Option<bool> {
1317
+ self.dom.custom_pseudo(id, name, argument)
1318
+ }
1319
+ fn pseudo_alias(&self, name: &str) -> Option<&str> {
1320
+ self.dom.pseudo_alias(name)
1321
+ }
1322
+ fn elements_named(&self, lowercase_name: &str) -> Option<&[u32]> {
1323
+ self.dom.elements_named(lowercase_name)
1324
+ }
1325
+ fn is_id_order_document_order(&self) -> bool {
1326
+ self.dom.is_id_order_document_order()
1327
+ }
1328
+ }
1329
+
1100
1330
  pub(crate) fn select<D: SelectDom>(
1101
1331
  dom: &D,
1102
1332
  source: &str,
1103
1333
  contexts: Option<&[u32]>,
1104
1334
  ) -> Result<Vec<u32>> {
1335
+ select_in(&Flagged::new(dom), source, contexts)
1336
+ }
1337
+
1338
+ /// Filter a supplied selection, applying positional pseudos to that selection.
1339
+ pub(crate) fn filter<D: SelectDom>(dom: &D, source: &str, ids: &[u32]) -> Result<Vec<u32>> {
1340
+ filter_in(&Flagged::new(dom), source, ids)
1341
+ }
1342
+
1343
+ fn select_in<D: SelectDom>(dom: &D, source: &str, contexts: Option<&[u32]>) -> Result<Vec<u32>> {
1105
1344
  let scope = contexts.unwrap_or(&[0]);
1106
1345
  let complexes = parse(source)?;
1107
1346
  // A compound over one subtree already follows DOM order. Avoid walking
@@ -1111,10 +1350,68 @@ pub(crate) fn select<D: SelectDom>(
1111
1350
  && complexes[0].leading.is_none()
1112
1351
  && scope.len() == 1;
1113
1352
  let mut result = Vec::new();
1114
- for complex in complexes {
1115
- validate(dom, &complex)?;
1353
+ let mut is_ordered = complexes.len() == 1 && scope.len() == 1;
1354
+ for complex in complexes.iter() {
1355
+ validate(dom, complex)?;
1356
+ if is_backward_safe(complex)? {
1357
+ // As in Cheerio, results lie within the scope, and elements matched
1358
+ // by earlier compounds may be the scope itself but not above it.
1359
+ // Candidates are in document order, and each is tested once.
1360
+ let last = complex.parts.len() - 1;
1361
+ let mut memo = BackwardMemo::default();
1362
+ if let Some(named) = (scope == [0] && !tag(dom, 0))
1363
+ .then(|| elements_named_by(dom, &complex.parts[last]))
1364
+ .flatten()
1365
+ {
1366
+ for id in named {
1367
+ if matches_backwards_within(dom, *id, complex, last, scope, None, &mut memo)? {
1368
+ result.push(*id);
1369
+ }
1370
+ }
1371
+ continue;
1372
+ }
1373
+ if scope == [0] && !tag(dom, 0) {
1374
+ // Test each node while the walk has it in cache, without a candidate list.
1375
+ let mut failure = None;
1376
+ for_each_descendant(dom, 0, |id| {
1377
+ match matches_backwards_within(dom, id, complex, last, scope, None, &mut memo) {
1378
+ Ok(true) => result.push(id),
1379
+ Ok(false) => {}
1380
+ Err(error) => failure = Some(error),
1381
+ }
1382
+ failure.is_none()
1383
+ });
1384
+ if let Some(error) = failure {
1385
+ return Err(error);
1386
+ }
1387
+ continue;
1388
+ }
1389
+ let candidates = descendants(dom, scope);
1390
+ let allowed: Option<IdSet<u32>> = (scope != [0] || tag(dom, 0))
1391
+ .then(|| scope.iter().chain(&candidates).copied().collect());
1392
+ for id in candidates {
1393
+ if matches_backwards_within(
1394
+ dom,
1395
+ id,
1396
+ complex,
1397
+ last,
1398
+ scope,
1399
+ allowed.as_ref(),
1400
+ &mut memo,
1401
+ )? {
1402
+ result.push(id);
1403
+ }
1404
+ }
1405
+ continue;
1406
+ }
1407
+ is_ordered &= ordered_compound;
1408
+ let named = (scope == [0] && complex.parts.len() == 1 && !tag(dom, 0))
1409
+ .then(|| elements_named_by(dom, &complex.parts[0]))
1410
+ .flatten();
1116
1411
  let mut candidates = if let Some(leading) = complex.leading {
1117
1412
  expand(dom, scope, leading)
1413
+ } else if let Some(named) = named {
1414
+ named.to_vec()
1118
1415
  } else {
1119
1416
  descendants(dom, scope)
1120
1417
  };
@@ -1135,17 +1432,22 @@ pub(crate) fn select<D: SelectDom>(
1135
1432
  }
1136
1433
  result.extend(current);
1137
1434
  }
1138
- if ordered_compound {
1435
+ if is_ordered {
1436
+ return Ok(result);
1437
+ }
1438
+ if dom.is_id_order_document_order() {
1439
+ result.sort_unstable();
1440
+ result.dedup();
1139
1441
  return Ok(result);
1140
1442
  }
1141
- let wanted: HashSet<u32> = result.iter().copied().collect();
1443
+ let wanted: IdSet<u32> = result.iter().copied().collect();
1142
1444
  // Stable IDs are allocation IDs after mutations; obtain actual DOM order.
1143
1445
  let ordered = descendants(dom, &[0]);
1144
1446
  let mut sorted: Vec<u32> = ordered
1145
1447
  .into_iter()
1146
1448
  .filter(|id| wanted.contains(id))
1147
1449
  .collect();
1148
- let attached: HashSet<u32> = sorted.iter().copied().collect();
1450
+ let attached: IdSet<u32> = sorted.iter().copied().collect();
1149
1451
  sorted.extend(
1150
1452
  unique(result)
1151
1453
  .into_iter()
@@ -1154,17 +1456,17 @@ pub(crate) fn select<D: SelectDom>(
1154
1456
  Ok(sorted)
1155
1457
  }
1156
1458
 
1157
- /// Filter a supplied selection, applying positional pseudos to that selection.
1158
- pub(crate) fn filter<D: SelectDom>(dom: &D, source: &str, ids: &[u32]) -> Result<Vec<u32>> {
1159
- let mut wanted = HashSet::new();
1160
- for complex in parse(source)? {
1161
- validate(dom, &complex)?;
1459
+ fn filter_in<D: SelectDom>(dom: &D, source: &str, ids: &[u32]) -> Result<Vec<u32>> {
1460
+ let mut wanted = IdSet::default();
1461
+ for complex in parse(source)?.iter() {
1462
+ validate(dom, complex)?;
1162
1463
  let candidates = if complex.parts.len() == 1 {
1163
1464
  apply(dom, ids.to_vec(), &complex.parts[0], ids)?
1164
1465
  } else {
1165
1466
  let mut result = Vec::new();
1467
+ let mut memo = BackwardMemo::default();
1166
1468
  for id in ids {
1167
- if matches_backwards(dom, *id, &complex, complex.parts.len() - 1, ids)? {
1469
+ if matches_backwards(dom, *id, complex, complex.parts.len() - 1, ids, &mut memo)? {
1168
1470
  result.push(*id);
1169
1471
  }
1170
1472
  }
@@ -241,7 +241,20 @@ impl TreeSink for CompatibleSink {
241
241
  }
242
242
 
243
243
  fn reparent_children(&self, node: &NodeId, new_parent: &NodeId) {
244
- self.0.reparent_children(node, new_parent);
244
+ // scraper reparents with ego_tree's `reparent_from_id_append`, which
245
+ // only points the first and last moved child at their new parent;
246
+ // the adoption agency algorithm then loses nodes moved later.
247
+ let mut html = self.0 .0.borrow_mut();
248
+ let children: Vec<NodeId> = html
249
+ .tree
250
+ .get(*node)
251
+ .map(|parent| parent.children().map(|child| child.id()).collect())
252
+ .unwrap_or_default();
253
+ if let Some(mut parent) = html.tree.get_mut(*new_parent) {
254
+ for child in children {
255
+ parent.append_id(child);
256
+ }
257
+ }
245
258
  }
246
259
 
247
260
  fn is_mathml_annotation_xml_integration_point(&self, handle: &NodeId) -> bool {