rusteerio 0.1.1 → 1.0.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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +7 -0
- package/THIRD_PARTY_NOTICES.md +32 -1
- package/dist/{chunk-DKULX6VO.mjs → chunk-DMRD3FGC.mjs} +1 -1
- package/dist/chunk-DMRD3FGC.mjs.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.mjs +1 -1
- package/native/src/arena.rs +54 -535
- package/native/src/codec.rs +62 -18
- package/native/src/hook_sink.rs +77 -3
- package/native/src/lib.rs +245 -18
- package/native/src/markup_tokenizer.rs +668 -0
- package/native/src/tokenizer.rs +355 -199
- package/native/vendor/README.md +3 -2
- package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
- package/package.json +1 -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/router.ts +3 -2
- package/dist/chunk-DKULX6VO.mjs.map +0 -1
package/dist/slim.mjs
CHANGED
package/native/src/arena.rs
CHANGED
|
@@ -51,6 +51,15 @@ impl DomNode {
|
|
|
51
51
|
pub(crate) fn is_element(&self) -> bool {
|
|
52
52
|
matches!(self.kind.as_str(), "tag" | "script" | "style")
|
|
53
53
|
}
|
|
54
|
+
pub(crate) fn content_bytes(&self) -> usize {
|
|
55
|
+
let attributes: usize = self
|
|
56
|
+
.attribs
|
|
57
|
+
.values()
|
|
58
|
+
.filter_map(Value::as_str)
|
|
59
|
+
.map(str::len)
|
|
60
|
+
.sum();
|
|
61
|
+
self.data.as_ref().map_or(0, String::len) + attributes
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
#[derive(Clone, Debug)]
|
|
@@ -353,20 +362,36 @@ impl DomArena {
|
|
|
353
362
|
let name = node.name.as_deref().unwrap_or("");
|
|
354
363
|
output.push('<');
|
|
355
364
|
output.push_str(name);
|
|
356
|
-
|
|
365
|
+
// parse5 qualifies foreign attributes; XML and htmlparser2
|
|
366
|
+
// rendering write the stored attribute names unchanged.
|
|
367
|
+
let qualifiers = (!xml
|
|
368
|
+
&& options.get("_useHtmlParser2") != Some(&Value::Bool(true)))
|
|
369
|
+
.then(|| {
|
|
370
|
+
(
|
|
371
|
+
node.extras.get("attribsNamespace"),
|
|
372
|
+
node.extras.get("attribsPrefix"),
|
|
373
|
+
)
|
|
374
|
+
});
|
|
375
|
+
let attributes = super::js_property_order(
|
|
376
|
+
node.attribs
|
|
377
|
+
.iter()
|
|
378
|
+
.map(|(name, value)| (name.as_str(), value)),
|
|
379
|
+
);
|
|
380
|
+
for (name, value) in attributes {
|
|
357
381
|
output.push(' ');
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
382
|
+
match qualifiers {
|
|
383
|
+
Some((namespaces, prefixes)) => super::push_attribute_name(
|
|
384
|
+
&mut output,
|
|
385
|
+
name,
|
|
386
|
+
namespaces
|
|
387
|
+
.and_then(|map| map.get(name))
|
|
388
|
+
.and_then(Value::as_str),
|
|
389
|
+
prefixes
|
|
390
|
+
.and_then(|map| map.get(name))
|
|
391
|
+
.and_then(Value::as_str),
|
|
392
|
+
),
|
|
393
|
+
None => output.push_str(name),
|
|
368
394
|
}
|
|
369
|
-
output.push_str(name);
|
|
370
395
|
let value = value
|
|
371
396
|
.as_str()
|
|
372
397
|
.map(str::to_owned)
|
|
@@ -625,16 +650,25 @@ impl DomArena {
|
|
|
625
650
|
.filter(|id| self.node(*id).is_some_and(DomNode::is_element)),
|
|
626
651
|
),
|
|
627
652
|
"parents" | "ancestors" => {
|
|
653
|
+
// Like Cheerio, stop at the first document node. A template's
|
|
654
|
+
// contents live in a fragment, so they never report the
|
|
655
|
+
// template or its ancestors as their own.
|
|
628
656
|
let mut parent = node.parent;
|
|
629
657
|
let mut seen = HashSet::new();
|
|
630
658
|
while let Some(id) = parent {
|
|
631
659
|
if !seen.insert(id) {
|
|
632
660
|
break;
|
|
633
661
|
}
|
|
634
|
-
|
|
662
|
+
let Some(ancestor) = self.node(id) else {
|
|
663
|
+
break;
|
|
664
|
+
};
|
|
665
|
+
if ancestor.kind == "root" {
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
if ancestor.is_element() {
|
|
635
669
|
result.push(id);
|
|
636
670
|
}
|
|
637
|
-
parent =
|
|
671
|
+
parent = ancestor.parent;
|
|
638
672
|
}
|
|
639
673
|
}
|
|
640
674
|
"next" | "prev" | "nextAll" | "prevAll" => {
|
|
@@ -851,481 +885,30 @@ fn escape(value: &str, attribute: bool, xml: bool, utf8: bool, output: &mut Stri
|
|
|
851
885
|
}
|
|
852
886
|
|
|
853
887
|
pub(crate) fn parse_xml(source: &str, options: Value) -> DomArena {
|
|
854
|
-
|
|
855
|
-
let mut parents = vec![0u32];
|
|
856
|
-
parse_xml_segment(source, &mut arena, &mut parents, 0);
|
|
857
|
-
arena
|
|
888
|
+
crate::tokenizer::parse_markup(source, options)
|
|
858
889
|
}
|
|
859
890
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
let indices = arena.options.get("withStartIndices") == Some(&Value::Bool(true))
|
|
863
|
-
|| arena.options.get("withEndIndices") == Some(&Value::Bool(true));
|
|
864
|
-
let mut positions = if indices {
|
|
865
|
-
vec![0usize; source.len() + 1]
|
|
866
|
-
} else {
|
|
867
|
-
Vec::new()
|
|
868
|
-
};
|
|
869
|
-
let mut utf16 = 0;
|
|
870
|
-
let codec = arena.options.get("__utf16Codec") == Some(&Value::Bool(true));
|
|
871
|
-
let mut escaped = false;
|
|
872
|
-
if indices {
|
|
873
|
-
for (at, ch) in source.char_indices() {
|
|
874
|
-
for position in &mut positions[at..at + ch.len_utf8()] {
|
|
875
|
-
*position = utf16;
|
|
876
|
-
}
|
|
877
|
-
if codec && ch == crate::codec::ESCAPE && !escaped {
|
|
878
|
-
escaped = true;
|
|
879
|
-
} else {
|
|
880
|
-
utf16 += ch.len_utf16();
|
|
881
|
-
escaped = false;
|
|
882
|
-
}
|
|
883
|
-
positions[at + ch.len_utf8()] = utf16;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
let source_position = |index: usize| {
|
|
887
|
-
base + if indices {
|
|
888
|
-
positions[index.min(source.len())]
|
|
889
|
-
} else {
|
|
890
|
-
index
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
macro_rules! append_parsed {
|
|
894
|
-
($arena:expr,$parent:expr,$node:expr,$start:expr,$end:expr $(,)?) => {
|
|
895
|
-
append_parsed(
|
|
896
|
-
$arena,
|
|
897
|
-
$parent,
|
|
898
|
-
$node,
|
|
899
|
-
source_position($start),
|
|
900
|
-
source_position(($end).saturating_add(1)).saturating_sub(1),
|
|
901
|
-
)
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
let mut index = 0;
|
|
905
|
-
let bytes = source.as_bytes();
|
|
906
|
-
let decode = arena.options.get("decodeEntities") != Some(&Value::Bool(false));
|
|
907
|
-
while index < bytes.len() {
|
|
908
|
-
let start = index;
|
|
909
|
-
if !xml {
|
|
910
|
-
if let Some(name) = parents
|
|
911
|
-
.last()
|
|
912
|
-
.and_then(|id| arena.node(*id))
|
|
913
|
-
.and_then(|node| node.name.as_deref())
|
|
914
|
-
.filter(|name| matches!(*name, "script" | "style" | "xmp" | "textarea" | "title"))
|
|
915
|
-
.map(str::to_owned)
|
|
916
|
-
{
|
|
917
|
-
let closing = format!("</{name}");
|
|
918
|
-
let end = source.as_bytes()[index..]
|
|
919
|
-
.windows(closing.len())
|
|
920
|
-
.enumerate()
|
|
921
|
-
.find(|(offset, candidate)| {
|
|
922
|
-
candidate.eq_ignore_ascii_case(closing.as_bytes())
|
|
923
|
-
&& bytes
|
|
924
|
-
.get(index + offset + closing.len())
|
|
925
|
-
.is_none_or(|ch| *ch == b'>' || ch.is_ascii_whitespace())
|
|
926
|
-
})
|
|
927
|
-
.map_or(bytes.len(), |(offset, _)| index + offset);
|
|
928
|
-
if end > index {
|
|
929
|
-
let text = &source[index..end];
|
|
930
|
-
let mut node = DomNode::new("text");
|
|
931
|
-
node.data = Some(if decode && name == "title" {
|
|
932
|
-
decode_entities_mode(
|
|
933
|
-
text,
|
|
934
|
-
false,
|
|
935
|
-
false,
|
|
936
|
-
arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
|
|
937
|
-
)
|
|
938
|
-
} else {
|
|
939
|
-
text.into()
|
|
940
|
-
});
|
|
941
|
-
append_parsed!(
|
|
942
|
-
arena,
|
|
943
|
-
*parents.last().unwrap(),
|
|
944
|
-
node,
|
|
945
|
-
index,
|
|
946
|
-
end.saturating_sub(1),
|
|
947
|
-
);
|
|
948
|
-
index = end;
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if bytes[index] == b'<'
|
|
953
|
-
&& bytes.get(index + 1).is_some_and(|ch| {
|
|
954
|
-
!ch.is_ascii_alphabetic() && !matches!(*ch, b'!' | b'?' | b'/')
|
|
955
|
-
})
|
|
956
|
-
{
|
|
957
|
-
let mut node = DomNode::new("text");
|
|
958
|
-
node.data = Some("<".into());
|
|
959
|
-
append_parsed!(arena, *parents.last().unwrap(), node, index, index);
|
|
960
|
-
index += 1;
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
if bytes[index] != b'<' {
|
|
965
|
-
index = source[index..]
|
|
966
|
-
.find('<')
|
|
967
|
-
.map_or(bytes.len(), |offset| index + offset);
|
|
968
|
-
let text = &source[start..index];
|
|
969
|
-
let mut node = DomNode::new("text");
|
|
970
|
-
node.data = Some(if decode {
|
|
971
|
-
decode_entities_mode(
|
|
972
|
-
text,
|
|
973
|
-
xml,
|
|
974
|
-
false,
|
|
975
|
-
arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
|
|
976
|
-
)
|
|
977
|
-
} else {
|
|
978
|
-
text.into()
|
|
979
|
-
});
|
|
980
|
-
append_parsed!(
|
|
981
|
-
arena,
|
|
982
|
-
*parents.last().unwrap(),
|
|
983
|
-
node,
|
|
984
|
-
start,
|
|
985
|
-
index.saturating_sub(1),
|
|
986
|
-
);
|
|
987
|
-
continue;
|
|
988
|
-
}
|
|
989
|
-
if source[index..].starts_with("<!--") {
|
|
990
|
-
let end = source[index + 4..]
|
|
991
|
-
.find("-->")
|
|
992
|
-
.map(|offset| index + 4 + offset);
|
|
993
|
-
let data_end = end.unwrap_or(bytes.len());
|
|
994
|
-
index = end.map_or(bytes.len(), |end| end + 3);
|
|
995
|
-
let mut node = DomNode::new("comment");
|
|
996
|
-
node.data = Some(source[start + 4..data_end].into());
|
|
997
|
-
append_parsed!(
|
|
998
|
-
arena,
|
|
999
|
-
*parents.last().unwrap(),
|
|
1000
|
-
node,
|
|
1001
|
-
start,
|
|
1002
|
-
index.saturating_sub(1),
|
|
1003
|
-
);
|
|
1004
|
-
continue;
|
|
1005
|
-
}
|
|
1006
|
-
if source[index..].starts_with("<![CDATA[") {
|
|
1007
|
-
let end = source[index + 9..]
|
|
1008
|
-
.find("]]>")
|
|
1009
|
-
.map(|offset| index + 9 + offset);
|
|
1010
|
-
let data_end = end.unwrap_or(bytes.len());
|
|
1011
|
-
index = end.map_or(bytes.len(), |end| end + 3);
|
|
1012
|
-
if !xml && arena.options.get("recognizeCDATA") != Some(&Value::Bool(true)) {
|
|
1013
|
-
let mut node = DomNode::new("comment");
|
|
1014
|
-
node.data = Some(format!("[CDATA[{}]]", &source[start + 9..data_end]));
|
|
1015
|
-
append_parsed!(
|
|
1016
|
-
arena,
|
|
1017
|
-
*parents.last().unwrap(),
|
|
1018
|
-
node,
|
|
1019
|
-
start,
|
|
1020
|
-
index.saturating_sub(1)
|
|
1021
|
-
);
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
let id = append_parsed!(
|
|
1025
|
-
arena,
|
|
1026
|
-
*parents.last().unwrap(),
|
|
1027
|
-
DomNode::new("cdata"),
|
|
1028
|
-
start,
|
|
1029
|
-
index.saturating_sub(1),
|
|
1030
|
-
);
|
|
1031
|
-
let mut text = DomNode::new("text");
|
|
1032
|
-
text.data = Some(source[start + 9..data_end].into());
|
|
1033
|
-
append_parsed!(arena, id, text, start + 9, data_end.saturating_sub(1));
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
let mut cursor = index + 1;
|
|
1037
|
-
let mut quote = None;
|
|
1038
|
-
while cursor < bytes.len() {
|
|
1039
|
-
let ch = bytes[cursor];
|
|
1040
|
-
if let Some(q) = quote {
|
|
1041
|
-
if ch == q {
|
|
1042
|
-
quote = None;
|
|
1043
|
-
}
|
|
1044
|
-
} else if ch == b'\'' || ch == b'"' {
|
|
1045
|
-
quote = Some(ch);
|
|
1046
|
-
} else if ch == b'>' {
|
|
1047
|
-
break;
|
|
1048
|
-
}
|
|
1049
|
-
cursor += 1;
|
|
1050
|
-
}
|
|
1051
|
-
if cursor >= bytes.len() {
|
|
1052
|
-
let mut text = DomNode::new("text");
|
|
1053
|
-
text.data = Some(source[index..].into());
|
|
1054
|
-
append_parsed!(
|
|
1055
|
-
arena,
|
|
1056
|
-
*parents.last().unwrap(),
|
|
1057
|
-
text,
|
|
1058
|
-
start,
|
|
1059
|
-
bytes.len().saturating_sub(1),
|
|
1060
|
-
);
|
|
1061
|
-
break;
|
|
1062
|
-
}
|
|
1063
|
-
index = cursor + 1;
|
|
1064
|
-
let token = source[start + 1..cursor].trim();
|
|
1065
|
-
if token.starts_with('!') || token.starts_with('?') {
|
|
1066
|
-
let mut node = DomNode::new("directive");
|
|
1067
|
-
node.name = Some(token.split_whitespace().next().unwrap_or("").into());
|
|
1068
|
-
node.data = Some(token.into());
|
|
1069
|
-
append_parsed!(arena, *parents.last().unwrap(), node, start, cursor);
|
|
1070
|
-
continue;
|
|
1071
|
-
}
|
|
1072
|
-
if let Some(close) = token.strip_prefix('/') {
|
|
1073
|
-
let close = close.split_whitespace().next().unwrap_or("");
|
|
1074
|
-
if let Some(position) = parents.iter().rposition(|id| {
|
|
1075
|
-
arena.nodes[*id as usize]
|
|
1076
|
-
.name
|
|
1077
|
-
.as_deref()
|
|
1078
|
-
.is_some_and(|name| {
|
|
1079
|
-
if xml {
|
|
1080
|
-
name == close
|
|
1081
|
-
} else {
|
|
1082
|
-
name.eq_ignore_ascii_case(close)
|
|
1083
|
-
}
|
|
1084
|
-
})
|
|
1085
|
-
}) {
|
|
1086
|
-
for id in parents.drain(position..) {
|
|
1087
|
-
if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
1088
|
-
arena.nodes[id as usize]
|
|
1089
|
-
.extras
|
|
1090
|
-
.insert("endIndex".into(), json!(source_position(cursor)));
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1096
|
-
let slash = token.ends_with('/');
|
|
1097
|
-
let content = token.strip_suffix('/').unwrap_or(token);
|
|
1098
|
-
let name_end = content.find(char::is_whitespace).unwrap_or(content.len());
|
|
1099
|
-
let name = &content[..name_end];
|
|
1100
|
-
if name.is_empty() {
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
let lower = arena
|
|
1104
|
-
.options
|
|
1105
|
-
.get("lowerCaseTags")
|
|
1106
|
-
.and_then(Value::as_bool)
|
|
1107
|
-
.unwrap_or(!xml);
|
|
1108
|
-
let name = if lower {
|
|
1109
|
-
name.to_ascii_lowercase()
|
|
1110
|
-
} else {
|
|
1111
|
-
name.into()
|
|
1112
|
-
};
|
|
1113
|
-
if !xml {
|
|
1114
|
-
while parents
|
|
1115
|
-
.last()
|
|
1116
|
-
.and_then(|id| arena.node(*id))
|
|
1117
|
-
.and_then(|node| node.name.as_deref())
|
|
1118
|
-
.is_some_and(|open| implied_close(&name, open))
|
|
1119
|
-
{
|
|
1120
|
-
let id = parents.pop().unwrap();
|
|
1121
|
-
if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
1122
|
-
arena.nodes[id as usize].extras.insert(
|
|
1123
|
-
"endIndex".into(),
|
|
1124
|
-
json!(source_position(start.saturating_sub(1))),
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
let foreign = matches!(name.as_str(), "svg" | "math")
|
|
1130
|
-
|| parents
|
|
1131
|
-
.iter()
|
|
1132
|
-
.rev()
|
|
1133
|
-
.filter_map(|id| arena.node(*id))
|
|
1134
|
-
.filter_map(|node| node.name.as_deref())
|
|
1135
|
-
.find_map(|name| {
|
|
1136
|
-
if matches!(
|
|
1137
|
-
name,
|
|
1138
|
-
"mi" | "mo"
|
|
1139
|
-
| "mn"
|
|
1140
|
-
| "ms"
|
|
1141
|
-
| "mtext"
|
|
1142
|
-
| "annotation-xml"
|
|
1143
|
-
| "foreignobject"
|
|
1144
|
-
| "desc"
|
|
1145
|
-
| "title"
|
|
1146
|
-
) {
|
|
1147
|
-
Some(false)
|
|
1148
|
-
} else if matches!(name, "svg" | "math") {
|
|
1149
|
-
Some(true)
|
|
1150
|
-
} else {
|
|
1151
|
-
None
|
|
1152
|
-
}
|
|
1153
|
-
})
|
|
1154
|
-
.unwrap_or(false);
|
|
1155
|
-
let self_close = slash
|
|
1156
|
-
&& (xml
|
|
1157
|
-
|| foreign
|
|
1158
|
-
|| arena.options.get("recognizeSelfClosing") == Some(&Value::Bool(true)));
|
|
1159
|
-
let mut node = DomNode::new(if !xml && name == "script" {
|
|
1160
|
-
"script"
|
|
1161
|
-
} else if !xml && name == "style" {
|
|
1162
|
-
"style"
|
|
1163
|
-
} else {
|
|
1164
|
-
"tag"
|
|
1165
|
-
});
|
|
1166
|
-
node.name = Some(name.clone());
|
|
1167
|
-
node.attribs = parse_attributes(
|
|
1168
|
-
&content[name_end..],
|
|
1169
|
-
decode,
|
|
1170
|
-
arena
|
|
1171
|
-
.options
|
|
1172
|
-
.get("lowerCaseAttributeNames")
|
|
1173
|
-
.and_then(Value::as_bool)
|
|
1174
|
-
.unwrap_or(!xml),
|
|
1175
|
-
xml,
|
|
1176
|
-
arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
|
|
1177
|
-
);
|
|
1178
|
-
let id = append_parsed!(arena, *parents.last().unwrap(), node, start, cursor);
|
|
1179
|
-
let void = !xml && html_parser_void(&name);
|
|
1180
|
-
if !self_close && !void {
|
|
1181
|
-
parents.push(id);
|
|
1182
|
-
} else if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
1183
|
-
arena.nodes[id as usize]
|
|
1184
|
-
.extras
|
|
1185
|
-
.insert("endIndex".into(), json!(source_position(cursor)));
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/// Incremental HTML-parser2/XML state; only incomplete markup/entity tokens are
|
|
1191
|
-
/// buffered, while completed input is discarded after native tree construction.
|
|
891
|
+
/// Incremental htmlparser2/XML input. The complete input is tokenized once, so
|
|
892
|
+
/// tags and entities split across chunks parse exactly as one string does.
|
|
1192
893
|
pub(crate) struct XmlStream {
|
|
1193
894
|
pub arena: DomArena,
|
|
1194
|
-
parents: Vec<u32>,
|
|
1195
895
|
pending: String,
|
|
1196
|
-
offset: usize,
|
|
1197
896
|
}
|
|
1198
897
|
impl XmlStream {
|
|
1199
898
|
pub(crate) fn new(options: Value) -> Self {
|
|
1200
899
|
Self {
|
|
1201
900
|
arena: DomArena::from_records(vec![DomNode::new("root")], options),
|
|
1202
|
-
parents: vec![0],
|
|
1203
901
|
pending: String::new(),
|
|
1204
|
-
offset: 0,
|
|
1205
902
|
}
|
|
1206
903
|
}
|
|
1207
904
|
pub(crate) fn feed(&mut self, chunk: &str, finish: bool) {
|
|
1208
905
|
self.pending.push_str(chunk);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
let mut length = if finish {
|
|
1213
|
-
input.len()
|
|
1214
|
-
} else {
|
|
1215
|
-
self.complete_prefix(input)
|
|
1216
|
-
};
|
|
1217
|
-
if !finish
|
|
1218
|
-
&& self.arena.options.get("__utf16Codec") == Some(&Value::Bool(true))
|
|
1219
|
-
&& input[..length]
|
|
1220
|
-
.chars()
|
|
1221
|
-
.rev()
|
|
1222
|
-
.take_while(|ch| *ch == crate::codec::ESCAPE)
|
|
1223
|
-
.count()
|
|
1224
|
-
% 2
|
|
1225
|
-
== 1
|
|
1226
|
-
{
|
|
1227
|
-
length -= crate::codec::ESCAPE.len_utf8();
|
|
1228
|
-
}
|
|
1229
|
-
if length == 0 {
|
|
1230
|
-
break;
|
|
1231
|
-
}
|
|
1232
|
-
let segment = &input[..length];
|
|
1233
|
-
parse_xml_segment(segment, &mut self.arena, &mut self.parents, self.offset);
|
|
1234
|
-
self.offset += if self.arena.options.get("__utf16Codec") == Some(&Value::Bool(true)) {
|
|
1235
|
-
crate::codec::logical_len(segment)
|
|
1236
|
-
} else {
|
|
1237
|
-
segment.encode_utf16().count()
|
|
1238
|
-
};
|
|
1239
|
-
consumed += length;
|
|
1240
|
-
if consumed == self.pending.len() || finish {
|
|
1241
|
-
break;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
self.pending.drain(..consumed);
|
|
1245
|
-
}
|
|
1246
|
-
fn complete_prefix(&self, input: &str) -> usize {
|
|
1247
|
-
let bytes = input.as_bytes();
|
|
1248
|
-
if input.is_empty() {
|
|
1249
|
-
return 0;
|
|
1250
|
-
}
|
|
1251
|
-
if !self.arena.xml_mode() {
|
|
1252
|
-
if let Some(name) = self
|
|
1253
|
-
.parents
|
|
1254
|
-
.last()
|
|
1255
|
-
.and_then(|id| self.arena.node(*id))
|
|
1256
|
-
.and_then(|node| node.name.as_deref())
|
|
1257
|
-
.filter(|name| matches!(*name, "script" | "style" | "xmp" | "textarea" | "title"))
|
|
1258
|
-
{
|
|
1259
|
-
let close = format!("</{name}");
|
|
1260
|
-
let closing = bytes
|
|
1261
|
-
.windows(close.len())
|
|
1262
|
-
.enumerate()
|
|
1263
|
-
.find(|(at, value)| {
|
|
1264
|
-
value.eq_ignore_ascii_case(close.as_bytes())
|
|
1265
|
-
&& bytes
|
|
1266
|
-
.get(at + close.len())
|
|
1267
|
-
.is_none_or(|ch| *ch == b'>' || ch.is_ascii_whitespace())
|
|
1268
|
-
})
|
|
1269
|
-
.map(|(at, _)| at);
|
|
1270
|
-
match closing {
|
|
1271
|
-
Some(0) if bytes.len() == close.len() => return 0,
|
|
1272
|
-
Some(0) => {}
|
|
1273
|
-
Some(index) => return safe_entity_prefix(input, index, name == "title"),
|
|
1274
|
-
None => {
|
|
1275
|
-
let mut end = input.len().saturating_sub(close.len() + 1);
|
|
1276
|
-
while !input.is_char_boundary(end) {
|
|
1277
|
-
end -= 1;
|
|
1278
|
-
}
|
|
1279
|
-
return safe_entity_prefix(input, end, name == "title");
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
if bytes[0] != b'<' {
|
|
1285
|
-
let end = input.find('<').unwrap_or(input.len());
|
|
1286
|
-
return safe_entity_prefix(
|
|
1287
|
-
input,
|
|
1288
|
-
end,
|
|
1289
|
-
self.arena.options.get("decodeEntities") != Some(&Value::Bool(false)),
|
|
1290
|
-
);
|
|
906
|
+
if finish {
|
|
907
|
+
let options = self.arena.options.clone();
|
|
908
|
+
self.arena = parse_xml(&std::mem::take(&mut self.pending), options);
|
|
1291
909
|
}
|
|
1292
|
-
if input.starts_with("<!--") {
|
|
1293
|
-
return input.find("-->").map_or(0, |index| index + 3);
|
|
1294
|
-
}
|
|
1295
|
-
if input.starts_with("<![CDATA[") {
|
|
1296
|
-
return input.find("]]>").map_or(0, |index| index + 3);
|
|
1297
|
-
}
|
|
1298
|
-
let mut quote = None;
|
|
1299
|
-
for (index, ch) in bytes.iter().copied().enumerate().skip(1) {
|
|
1300
|
-
if let Some(q) = quote {
|
|
1301
|
-
if ch == q {
|
|
1302
|
-
quote = None;
|
|
1303
|
-
}
|
|
1304
|
-
} else if ch == b'\'' || ch == b'"' {
|
|
1305
|
-
quote = Some(ch);
|
|
1306
|
-
} else if ch == b'>' {
|
|
1307
|
-
return index + 1;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
0
|
|
1311
910
|
}
|
|
1312
911
|
}
|
|
1313
|
-
fn safe_entity_prefix(source: &str, end: usize, decode: bool) -> usize {
|
|
1314
|
-
if decode {
|
|
1315
|
-
if let Some(start) = source[..end].rfind('&') {
|
|
1316
|
-
let tail = &source[start + 1..end];
|
|
1317
|
-
if tail.len() < 64
|
|
1318
|
-
&& tail
|
|
1319
|
-
.chars()
|
|
1320
|
-
.all(|ch| ch.is_ascii_alphanumeric() || ch == '#')
|
|
1321
|
-
{
|
|
1322
|
-
return start;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
end
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
912
|
pub(crate) fn append_parsed(
|
|
1330
913
|
arena: &mut DomArena,
|
|
1331
914
|
parent: u32,
|
|
@@ -1395,70 +978,6 @@ pub(crate) fn implied_close(name: &str, open: &str) -> bool {
|
|
|
1395
978
|
_ => false,
|
|
1396
979
|
}
|
|
1397
980
|
}
|
|
1398
|
-
fn parse_attributes(
|
|
1399
|
-
source: &str,
|
|
1400
|
-
decode: bool,
|
|
1401
|
-
lower: bool,
|
|
1402
|
-
xml: bool,
|
|
1403
|
-
codec: bool,
|
|
1404
|
-
) -> Map<String, Value> {
|
|
1405
|
-
let bytes = source.as_bytes();
|
|
1406
|
-
let mut index = 0;
|
|
1407
|
-
let mut attrs = Map::new();
|
|
1408
|
-
while index < bytes.len() {
|
|
1409
|
-
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
1410
|
-
index += 1;
|
|
1411
|
-
}
|
|
1412
|
-
let start = index;
|
|
1413
|
-
while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'=' {
|
|
1414
|
-
index += 1;
|
|
1415
|
-
}
|
|
1416
|
-
if start == index {
|
|
1417
|
-
index += 1;
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
let name = &source[start..index];
|
|
1421
|
-
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
1422
|
-
index += 1;
|
|
1423
|
-
}
|
|
1424
|
-
let mut value = "";
|
|
1425
|
-
if bytes.get(index) == Some(&b'=') {
|
|
1426
|
-
index += 1;
|
|
1427
|
-
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
1428
|
-
index += 1;
|
|
1429
|
-
}
|
|
1430
|
-
if matches!(bytes.get(index), Some(b'\'' | b'"')) {
|
|
1431
|
-
let quote = bytes[index];
|
|
1432
|
-
index += 1;
|
|
1433
|
-
let start = index;
|
|
1434
|
-
while index < bytes.len() && bytes[index] != quote {
|
|
1435
|
-
index += 1;
|
|
1436
|
-
}
|
|
1437
|
-
value = &source[start..index];
|
|
1438
|
-
index += (index < bytes.len()) as usize;
|
|
1439
|
-
} else {
|
|
1440
|
-
let start = index;
|
|
1441
|
-
while index < bytes.len() && !bytes[index].is_ascii_whitespace() {
|
|
1442
|
-
index += 1;
|
|
1443
|
-
}
|
|
1444
|
-
value = &source[start..index];
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
let name = if lower {
|
|
1448
|
-
name.to_ascii_lowercase()
|
|
1449
|
-
} else {
|
|
1450
|
-
name.into()
|
|
1451
|
-
};
|
|
1452
|
-
attrs.entry(name).or_insert_with(|| {
|
|
1453
|
-
Value::String(if decode {
|
|
1454
|
-
decode_entities_mode(value, xml, true, codec)
|
|
1455
|
-
} else {
|
|
1456
|
-
value.into()
|
|
1457
|
-
})
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
attrs
|
|
1461
|
-
}
|
|
1462
981
|
pub(crate) fn decode_entities_mode(
|
|
1463
982
|
source: &str,
|
|
1464
983
|
xml: bool,
|