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/dist/slim.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  dom,
4
4
  load,
5
5
  merge
6
- } from "./chunk-DKULX6VO.mjs";
6
+ } from "./chunk-DMRD3FGC.mjs";
7
7
  import "./chunk-OHPGEY3E.mjs";
8
8
 
9
9
  // src/slim.ts
@@ -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
- for (name, value) in &node.attribs {
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
- if !xml {
359
- if let Some(prefix) = node
360
- .extras
361
- .get("attribsPrefix")
362
- .and_then(|value| value.get(name))
363
- .and_then(Value::as_str)
364
- {
365
- output.push_str(prefix);
366
- output.push(':');
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
- if self.node(id).is_some_and(DomNode::is_element) {
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 = self.node(id).and_then(|node| node.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
- let mut arena = DomArena::from_records(vec![DomNode::new("root")], options);
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
- fn parse_xml_segment(source: &str, arena: &mut DomArena, parents: &mut Vec<u32>, base: usize) {
861
- let xml = arena.xml_mode();
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
- let mut consumed = 0;
1210
- loop {
1211
- let input = &self.pending[consumed..];
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,