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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/THIRD_PARTY_NOTICES.md +32 -1
- package/native/src/arena.rs +12 -522
- package/native/src/codec.rs +62 -18
- package/native/src/hook_sink.rs +77 -3
- package/native/src/lib.rs +58 -1
- 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/native/src/codec.rs
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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))
|
package/native/src/hook_sink.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|