rusteerio 1.0.0 → 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.
@@ -10,6 +10,9 @@ pub(crate) fn has_encoding(value: &str) -> bool {
10
10
  value.chars().any(is_reserved)
11
11
  }
12
12
  pub(crate) fn from_units(units: &[u16]) -> String {
13
+ if units.iter().all(|unit| !(0xd800..=0xe800).contains(unit)) {
14
+ return String::from_utf16_lossy(units);
15
+ }
13
16
  let mut output = String::with_capacity(units.len());
14
17
  let mut index = 0;
15
18
  while index < units.len() {
@@ -38,28 +41,69 @@ pub(crate) fn from_units(units: &[u16]) -> String {
38
41
  }
39
42
  output
40
43
  }
41
- pub(crate) fn to_units(value: &str) -> Vec<u16> {
42
- let mut output = Vec::with_capacity(value.len());
43
- let mut chars = value.chars().peekable();
44
- while let Some(ch) = chars.next() {
45
- if ch == ESCAPE && chars.peek().copied().is_some_and(is_reserved) {
46
- output.push(chars.next().unwrap() as u16);
47
- } else if ('\u{e000}'..'\u{e800}').contains(&ch) {
48
- output.push(0xd800 + (ch as u16 - 0xe000));
49
- } else {
50
- let mut encoded = [0; 2];
51
- output.extend_from_slice(ch.encode_utf16(&mut encoded));
52
- }
44
+ /// Receives the UTF-16 units of a string from [`visit_units`].
45
+ pub(crate) trait UnitSink {
46
+ /// A run of ASCII bytes starting at byte offset `start`, one unit each.
47
+ fn ascii(&mut self, start: usize, run: &[u8]);
48
+ /// One unit and the byte offset of the character that starts there, or
49
+ /// `None` for the low half of a surrogate pair.
50
+ fn unit(&mut self, offset: Option<usize>, unit: u16);
51
+ }
52
+
53
+ impl UnitSink for Vec<u16> {
54
+ fn ascii(&mut self, _: usize, run: &[u8]) {
55
+ self.extend(run.iter().map(|value| u16::from(*value)));
56
+ }
57
+ fn unit(&mut self, _: Option<usize>, unit: u16) {
58
+ self.push(unit);
53
59
  }
54
- output
55
60
  }
56
- pub(crate) fn logical_len(value: &str) -> usize {
57
- if has_encoding(value) {
58
- to_units(value).len()
59
- } else {
60
- value.encode_utf16().count()
61
+
62
+ /// Feeds the UTF-16 units of `value` to `sink`. With `framed`, lossless frames
63
+ /// decode to their original units.
64
+ pub(crate) fn visit_units(value: &str, framed: bool, sink: &mut impl UnitSink) {
65
+ let bytes = value.as_bytes();
66
+ let mut byte = 0;
67
+ while byte < bytes.len() {
68
+ let ascii = bytes[byte..]
69
+ .iter()
70
+ .position(|value| !value.is_ascii())
71
+ .unwrap_or(bytes.len() - byte);
72
+ sink.ascii(byte, &bytes[byte..byte + ascii]);
73
+ byte += ascii;
74
+ let Some(ch) = value[byte..].chars().next() else {
75
+ break;
76
+ };
77
+ let start = byte;
78
+ byte += ch.len_utf8();
79
+ if framed && ch == ESCAPE {
80
+ if let Some(next) = value[byte..]
81
+ .chars()
82
+ .next()
83
+ .filter(|next| is_reserved(*next))
84
+ {
85
+ sink.unit(Some(start), next as u16);
86
+ byte += next.len_utf8();
87
+ continue;
88
+ }
89
+ }
90
+ if framed && ('\u{e000}'..ESCAPE).contains(&ch) {
91
+ sink.unit(Some(start), 0xd800 + (ch as u16 - 0xe000));
92
+ continue;
93
+ }
94
+ let mut encoded = [0; 2];
95
+ let encoded = ch.encode_utf16(&mut encoded);
96
+ sink.unit(Some(start), encoded[0]);
97
+ if let Some(low) = encoded.get(1) {
98
+ sink.unit(None, *low);
99
+ }
61
100
  }
62
101
  }
102
+ pub(crate) fn to_units(value: &str) -> Vec<u16> {
103
+ let mut output = Vec::with_capacity(value.len());
104
+ visit_units(value, has_encoding(value), &mut output);
105
+ output
106
+ }
63
107
  pub(crate) fn to_usv(value: &str) -> String {
64
108
  if has_encoding(value) {
65
109
  String::from_utf16_lossy(&to_units(value))
@@ -1,7 +1,7 @@
1
1
  //! An active user-supplied tree adapter. Rust runs the HTML5 algorithm; the
2
2
  //! adapter supplies every construction, mutation and tree-inspection decision.
3
3
  use std::borrow::Cow;
4
- use std::cell::{Ref, RefCell};
4
+ use std::cell::{Cell, Ref, RefCell};
5
5
  use std::collections::HashMap;
6
6
  use std::rc::Rc;
7
7
 
@@ -196,6 +196,26 @@ struct HookSink {
196
196
  context: Option<Rc<crate::diagnostics::DiagnosticContext>>,
197
197
  diagnostics: RefCell<Vec<Value>>,
198
198
  locations: Option<crate::tracker::Locations<u32>>,
199
+ pending_escape: Cell<Option<TextTarget>>,
200
+ }
201
+
202
+ /// Where a text chunk is inserted: appended to `parent`, or before `sibling`.
203
+ #[derive(Clone, Copy, PartialEq, Eq)]
204
+ struct TextTarget {
205
+ parent: u32,
206
+ sibling: Option<u32>,
207
+ }
208
+
209
+ /// Whether the final character is a codec escape whose character has not
210
+ /// arrived yet. Escapes pair with the following character.
211
+ fn ends_with_unpaired_escape(text: &str) -> bool {
212
+ let mut chars = text.chars();
213
+ while let Some(ch) = chars.next() {
214
+ if ch == crate::codec::ESCAPE && chars.next().is_none() {
215
+ return true;
216
+ }
217
+ }
218
+ false
199
219
  }
200
220
 
201
221
  impl HookSink {
@@ -203,6 +223,17 @@ impl HookSink {
203
223
  if self.error.borrow().is_some() {
204
224
  return Value::Null;
205
225
  }
226
+ if matches!(
227
+ method,
228
+ "appendChild"
229
+ | "insertBefore"
230
+ | "detachNode"
231
+ | "setDocumentType"
232
+ | "setTemplateContent"
233
+ | "__snapshot"
234
+ ) {
235
+ self.flush_escape();
236
+ }
206
237
  let result = self.bridge.call(method, args);
207
238
  match result {
208
239
  Ok(value) => value,
@@ -215,6 +246,38 @@ impl HookSink {
215
246
  fn node(&self, method: &str, args: Value) -> u32 {
216
247
  self.call(method, args).as_u64().unwrap_or(0) as u32
217
248
  }
249
+ /// The tokenizer may split a codec escape from its character across text
250
+ /// chunks. Adapters decode each chunk separately, so a trailing escape is
251
+ /// held until the next chunk for the same target.
252
+ fn insert_text(&self, target: TextTarget, text: &str) {
253
+ let mut data = String::with_capacity(text.len() + crate::codec::ESCAPE.len_utf8());
254
+ if let Some(pending) = self.pending_escape.take() {
255
+ if pending == target {
256
+ data.push(crate::codec::ESCAPE);
257
+ } else {
258
+ self.send_text(pending, &crate::codec::ESCAPE.to_string());
259
+ }
260
+ }
261
+ data.push_str(text);
262
+ if ends_with_unpaired_escape(&data) {
263
+ data.pop();
264
+ self.pending_escape.set(Some(target));
265
+ }
266
+ if !data.is_empty() {
267
+ self.send_text(target, &data);
268
+ }
269
+ }
270
+ fn send_text(&self, target: TextTarget, data: &str) {
271
+ match target.sibling {
272
+ None => self.call("insertText", json!([target.parent, data])),
273
+ Some(sibling) => self.call("insertTextBefore", json!([target.parent, data, sibling])),
274
+ };
275
+ }
276
+ fn flush_escape(&self) {
277
+ if let Some(pending) = self.pending_escape.take() {
278
+ self.send_text(pending, &crate::codec::ESCAPE.to_string());
279
+ }
280
+ }
218
281
  fn attrs(attrs: Vec<Attribute>) -> Value {
219
282
  Value::Array(
220
283
  attrs
@@ -463,7 +526,11 @@ impl TreeSink for HookSink {
463
526
  }
464
527
  }
465
528
  NodeOrText::AppendText(text) => {
466
- self.call("insertText", json!([parent, text.as_ref()]));
529
+ let target = TextTarget {
530
+ parent: *parent,
531
+ sibling: None,
532
+ };
533
+ self.insert_text(target, text.as_ref());
467
534
  self.mark_text(*parent, None, text.as_ref());
468
535
  }
469
536
  }
@@ -530,7 +597,11 @@ impl TreeSink for HookSink {
530
597
  self.call("insertBefore", json!([parent, node, sibling]));
531
598
  }
532
599
  NodeOrText::AppendText(text) => {
533
- self.call("insertTextBefore", json!([parent, text.as_ref(), sibling]));
600
+ let target = TextTarget {
601
+ parent,
602
+ sibling: Some(*sibling),
603
+ };
604
+ self.insert_text(target, text.as_ref());
534
605
  self.mark_text(parent, Some(*sibling), text.as_ref());
535
606
  }
536
607
  }
@@ -589,6 +660,7 @@ fn parser(
589
660
  context: None,
590
661
  diagnostics: RefCell::new(Vec::new()),
591
662
  locations: None,
663
+ pending_escape: Cell::new(None),
592
664
  };
593
665
  sink.call(
594
666
  if is_document {
@@ -659,6 +731,7 @@ pub(crate) fn parse(
659
731
  diagnostics: RefCell::new(Vec::new()),
660
732
  locations: (options.get("sourceCodeLocationInfo") == Some(&Value::Bool(true)))
661
733
  .then(|| crate::tracker::Locations::new(context.clone())),
734
+ pending_escape: Cell::new(None),
662
735
  };
663
736
  sink.call(
664
737
  if is_document {
@@ -706,6 +779,7 @@ where
706
779
  diagnostics: RefCell::new(Vec::new()),
707
780
  locations: (options.get("sourceCodeLocationInfo") == Some(&Value::Bool(true)))
708
781
  .then(|| crate::tracker::Locations::new(context.clone())),
782
+ pending_escape: Cell::new(None),
709
783
  };
710
784
  sink.call(
711
785
  if is_document {
package/native/src/lib.rs CHANGED
@@ -23,6 +23,7 @@ mod gc;
23
23
  mod hook_sink;
24
24
  mod hook_stream;
25
25
  mod location;
26
+ mod markup_tokenizer;
26
27
  mod runtime;
27
28
  mod selector;
28
29
  mod sink;
@@ -68,6 +69,56 @@ pub(crate) fn push_attribute_name(
68
69
  output.push_str(local);
69
70
  }
70
71
 
72
+ /// Orders attributes the way a JavaScript object enumerates its keys: integer
73
+ /// keys first, ascending, then insertion order. Cheerio serializes attributes
74
+ /// from such an object.
75
+ pub(crate) fn js_property_order<'a, T, I>(entries: I) -> JsPropertyOrder<I>
76
+ where
77
+ I: Iterator<Item = (&'a str, T)> + Clone,
78
+ {
79
+ if !entries.clone().any(|(name, _)| array_index(name).is_some()) {
80
+ return JsPropertyOrder::Unchanged(entries);
81
+ }
82
+ let mut keyed: Vec<_> = entries
83
+ .map(|entry| {
84
+ (
85
+ array_index(entry.0).map_or((1, 0), |index| (0, index)),
86
+ entry,
87
+ )
88
+ })
89
+ .collect();
90
+ keyed.sort_by_key(|(key, _)| *key);
91
+ let sorted: Vec<_> = keyed.into_iter().map(|(_, entry)| entry).collect();
92
+ JsPropertyOrder::Sorted(sorted.into_iter())
93
+ }
94
+
95
+ pub(crate) enum JsPropertyOrder<I: Iterator> {
96
+ Unchanged(I),
97
+ Sorted(std::vec::IntoIter<I::Item>),
98
+ }
99
+
100
+ impl<I: Iterator> Iterator for JsPropertyOrder<I> {
101
+ type Item = I::Item;
102
+ fn next(&mut self) -> Option<Self::Item> {
103
+ match self {
104
+ Self::Unchanged(entries) => entries.next(),
105
+ Self::Sorted(entries) => entries.next(),
106
+ }
107
+ }
108
+ }
109
+
110
+ fn array_index(name: &str) -> Option<u32> {
111
+ let is_canonical = !name.is_empty()
112
+ && name.len() <= 10
113
+ && name.bytes().all(|byte| byte.is_ascii_digit())
114
+ && (name == "0" || !name.starts_with('0'));
115
+ is_canonical
116
+ .then(|| name.parse::<u64>().ok())
117
+ .flatten()
118
+ .filter(|value| *value < u64::from(u32::MAX))
119
+ .map(|value| value as u32)
120
+ }
121
+
71
122
  #[napi(custom_finalize)]
72
123
  pub struct NativeDocument {
73
124
  html: RefCell<Option<Html>>,
@@ -1323,7 +1374,13 @@ fn serialize_node(node: NodeRef<'_, Node>, output: &mut String) {
1323
1374
  Node::Element(element) => {
1324
1375
  output.push('<');
1325
1376
  output.push_str(element.name());
1326
- for (name, value) in &element.attrs {
1377
+ let attributes = js_property_order(
1378
+ element
1379
+ .attrs
1380
+ .iter()
1381
+ .map(|(name, value)| (name.local.as_ref(), (name, value))),
1382
+ );
1383
+ for (_, (name, value)) in attributes {
1327
1384
  output.push(' ');
1328
1385
  push_attribute_name(
1329
1386
  output,