rusteerio 1.0.1 → 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 +107 -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 +219 -80
- package/native/src/attributes.rs +11 -5
- 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/id_hash.rs +42 -0
- package/native/src/immutable.rs +256 -0
- package/native/src/lib.rs +1039 -171
- 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 +29 -12
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
- 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/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/selector.rs
CHANGED
|
@@ -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,
|
|
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
|
-
|| (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
749
|
+
if position(name)
|
|
645
750
|
|| (matches!(name.as_str(), "not" | "is" | "where" | "matches")
|
|
646
|
-
&& has_position(
|
|
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
|
-
&&
|
|
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
|
|
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
|
-
|
|
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" => !
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
1115
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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,
|
|
1469
|
+
if matches_backwards(dom, *id, complex, complex.parts.len() - 1, ids, &mut memo)? {
|
|
1168
1470
|
result.push(*id);
|
|
1169
1471
|
}
|
|
1170
1472
|
}
|
package/native/src/sink.rs
CHANGED
|
@@ -241,7 +241,20 @@ impl TreeSink for CompatibleSink {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
fn reparent_children(&self, node: &NodeId, new_parent: &NodeId) {
|
|
244
|
-
|
|
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 {
|