rusteerio 1.0.0 → 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 +138 -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 +231 -602
- package/native/src/attributes.rs +11 -5
- package/native/src/codec.rs +62 -18
- 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/hook_sink.rs +77 -3
- package/native/src/id_hash.rs +42 -0
- package/native/src/immutable.rs +256 -0
- package/native/src/lib.rs +1097 -172
- package/native/src/markup_tokenizer.rs +668 -0
- 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 +377 -204
- package/native/vendor/README.md +3 -2
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
- 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/scripts/generate-notices.mjs +11 -0
- package/scripts/notices/htmlparser2-MIT.txt +18 -0
- package/scripts/notices/sources.json +6 -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/tokenizer.rs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//!
|
|
1
|
+
//! Build htmlparser2-compatible trees from tokenizer events. Events come from
|
|
2
|
+
//! the native tokenizer port or from an explicitly supplied JavaScript
|
|
3
|
+
//! Tokenizer, whose recorded events directly determine the DOM.
|
|
3
4
|
use crate::arena::{append_parsed, html_parser_void, implied_close, DomArena, DomNode};
|
|
5
|
+
use crate::markup_tokenizer::Token;
|
|
4
6
|
use serde_json::{json, Map, Value};
|
|
5
7
|
|
|
6
8
|
struct Pending {
|
|
@@ -8,9 +10,9 @@ struct Pending {
|
|
|
8
10
|
attrs: Map<String, Value>,
|
|
9
11
|
start: usize,
|
|
10
12
|
}
|
|
11
|
-
struct Builder {
|
|
13
|
+
struct Builder<'a> {
|
|
12
14
|
arena: DomArena,
|
|
13
|
-
source:
|
|
15
|
+
source: &'a Source<'a>,
|
|
14
16
|
parents: Vec<u32>,
|
|
15
17
|
foreign: Vec<bool>,
|
|
16
18
|
pending: Option<Pending>,
|
|
@@ -20,29 +22,57 @@ struct Builder {
|
|
|
20
22
|
end: usize,
|
|
21
23
|
xml: bool,
|
|
22
24
|
lower_tags: bool,
|
|
25
|
+
lower_attrs: bool,
|
|
26
|
+
with_end_indices: bool,
|
|
27
|
+
recognize_self_closing: Option<bool>,
|
|
23
28
|
}
|
|
24
|
-
impl Builder {
|
|
29
|
+
impl<'a> Builder<'a> {
|
|
30
|
+
fn new(source: &'a Source<'a>, options: Value) -> Self {
|
|
31
|
+
let xml = crate::arena::xml_mode(&options);
|
|
32
|
+
let lower_tags = options
|
|
33
|
+
.get("lowerCaseTags")
|
|
34
|
+
.and_then(Value::as_bool)
|
|
35
|
+
.unwrap_or(!xml);
|
|
36
|
+
let lower_attrs = options
|
|
37
|
+
.get("lowerCaseAttributeNames")
|
|
38
|
+
.and_then(Value::as_bool)
|
|
39
|
+
.unwrap_or(!xml);
|
|
40
|
+
let with_end_indices = options.get("withEndIndices") == Some(&Value::Bool(true));
|
|
41
|
+
let recognize_self_closing = options.get("recognizeSelfClosing").and_then(Value::as_bool);
|
|
42
|
+
let mut arena = DomArena::from_records(vec![DomNode::new("root")], options);
|
|
43
|
+
// Growing the arena one doubling at a time copies every node record again.
|
|
44
|
+
arena.nodes.reserve(source.units.len() / 48);
|
|
45
|
+
Self {
|
|
46
|
+
arena,
|
|
47
|
+
source,
|
|
48
|
+
parents: vec![0],
|
|
49
|
+
foreign: vec![xml],
|
|
50
|
+
pending: None,
|
|
51
|
+
attribute_name: String::new(),
|
|
52
|
+
attribute_value: String::new(),
|
|
53
|
+
start: 0,
|
|
54
|
+
end: 0,
|
|
55
|
+
xml,
|
|
56
|
+
lower_tags,
|
|
57
|
+
lower_attrs,
|
|
58
|
+
with_end_indices,
|
|
59
|
+
recognize_self_closing,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
25
62
|
fn slice(&self, start: usize, end: usize) -> String {
|
|
26
|
-
|
|
27
|
-
self.source
|
|
28
|
-
.get(
|
|
29
|
-
start.min(self.source.len())
|
|
30
|
-
..end.min(self.source.len()).max(start.min(self.source.len())),
|
|
31
|
-
)
|
|
32
|
-
.unwrap_or_default(),
|
|
33
|
-
)
|
|
63
|
+
self.source.slice(start, end)
|
|
34
64
|
}
|
|
35
65
|
fn name(&self, start: usize, end: usize) -> String {
|
|
36
66
|
let name = self.slice(start, end);
|
|
37
67
|
if self.lower_tags {
|
|
38
|
-
name
|
|
68
|
+
lowercase(name)
|
|
39
69
|
} else {
|
|
40
70
|
name
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
fn close(&mut self) {
|
|
44
74
|
if let Some(id) = (self.parents.len() > 1).then(|| self.parents.pop().unwrap()) {
|
|
45
|
-
if self.
|
|
75
|
+
if self.with_end_indices {
|
|
46
76
|
self.arena.nodes[id as usize]
|
|
47
77
|
.extras
|
|
48
78
|
.insert("endIndex".into(), json!(self.end));
|
|
@@ -61,7 +91,8 @@ impl Builder {
|
|
|
61
91
|
self.close();
|
|
62
92
|
}
|
|
63
93
|
}
|
|
64
|
-
|
|
94
|
+
// htmlparser2 tracks foreign content only in HTML mode.
|
|
95
|
+
if !self.xml && !html_parser_void(&name) {
|
|
65
96
|
if matches!(name.as_str(), "svg" | "math") {
|
|
66
97
|
self.foreign.push(true);
|
|
67
98
|
} else if matches!(
|
|
@@ -87,12 +118,14 @@ impl Builder {
|
|
|
87
118
|
fn end_open(&mut self, self_closing: bool) {
|
|
88
119
|
if let Some(pending) = self.pending.take() {
|
|
89
120
|
let is_void = !self.xml && html_parser_void(&pending.name);
|
|
90
|
-
let kind =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"tag"
|
|
121
|
+
let kind = match pending.name.as_str() {
|
|
122
|
+
"script" if !self.xml => "script",
|
|
123
|
+
"style" if !self.xml => "style",
|
|
124
|
+
_ => "tag",
|
|
94
125
|
};
|
|
95
126
|
let mut node = DomNode::new(kind);
|
|
127
|
+
let lowercase_name = (pending.name.bytes().any(|byte| byte.is_ascii_uppercase()))
|
|
128
|
+
.then(|| pending.name.to_ascii_lowercase());
|
|
96
129
|
node.name = Some(pending.name);
|
|
97
130
|
node.attribs = pending.attrs;
|
|
98
131
|
let id = append_parsed(
|
|
@@ -102,6 +135,17 @@ impl Builder {
|
|
|
102
135
|
pending.start,
|
|
103
136
|
self.end,
|
|
104
137
|
);
|
|
138
|
+
let name = lowercase_name
|
|
139
|
+
.as_deref()
|
|
140
|
+
.or(self.arena.nodes[id as usize].name.as_deref())
|
|
141
|
+
.unwrap_or("");
|
|
142
|
+
let index = self.arena.name_index.get_or_insert_with(Default::default);
|
|
143
|
+
match index.get_mut(name) {
|
|
144
|
+
Some(ids) => ids.push(id),
|
|
145
|
+
None => {
|
|
146
|
+
index.insert(name.to_owned(), vec![id]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
105
149
|
if !is_void && !self_closing {
|
|
106
150
|
self.parents.push(id);
|
|
107
151
|
}
|
|
@@ -145,34 +189,107 @@ impl Builder {
|
|
|
145
189
|
}
|
|
146
190
|
}
|
|
147
191
|
|
|
192
|
+
/// Sources from JavaScript, or that contain codec frames, use the lossless
|
|
193
|
+
/// UTF-16 representation; others are plain Unicode.
|
|
194
|
+
fn is_codec_encoded(options: &Value) -> bool {
|
|
195
|
+
options.get("__utf16Codec") == Some(&Value::Bool(true))
|
|
196
|
+
|| options.get("__losslessUnicode") == Some(&Value::Bool(true))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn lowercase(mut name: String) -> String {
|
|
200
|
+
if name.is_ascii() {
|
|
201
|
+
name.make_ascii_lowercase();
|
|
202
|
+
name
|
|
203
|
+
} else {
|
|
204
|
+
name.to_lowercase()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// The source as UTF-16 units, with each unit's byte offset in the original
|
|
209
|
+
/// string so spans can be copied from it instead of re-encoded.
|
|
210
|
+
struct Source<'a> {
|
|
211
|
+
text: &'a str,
|
|
212
|
+
units: Vec<u16>,
|
|
213
|
+
/// Byte offset per unit index, plus one for the end; `u32::MAX` marks an
|
|
214
|
+
/// index inside a character. `None` for ASCII, where offsets are indices.
|
|
215
|
+
offsets: Option<Vec<u32>>,
|
|
216
|
+
codec: bool,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
struct IndexedUnits {
|
|
220
|
+
units: Vec<u16>,
|
|
221
|
+
offsets: Vec<u32>,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
impl crate::codec::UnitSink for IndexedUnits {
|
|
225
|
+
fn ascii(&mut self, start: usize, run: &[u8]) {
|
|
226
|
+
self.units.extend(run.iter().map(|value| u16::from(*value)));
|
|
227
|
+
self.offsets
|
|
228
|
+
.extend((start..start + run.len()).map(|offset| offset as u32));
|
|
229
|
+
}
|
|
230
|
+
fn unit(&mut self, offset: Option<usize>, unit: u16) {
|
|
231
|
+
self.units.push(unit);
|
|
232
|
+
self.offsets
|
|
233
|
+
.push(offset.map_or(u32::MAX, |offset| offset as u32));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
impl<'a> Source<'a> {
|
|
238
|
+
fn new(text: &'a str, options: &Value) -> Self {
|
|
239
|
+
let codec = is_codec_encoded(options);
|
|
240
|
+
if text.is_ascii() {
|
|
241
|
+
return Self {
|
|
242
|
+
text,
|
|
243
|
+
units: text.bytes().map(u16::from).collect(),
|
|
244
|
+
offsets: None,
|
|
245
|
+
codec,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
let mut indexed = IndexedUnits {
|
|
249
|
+
units: Vec::with_capacity(text.len()),
|
|
250
|
+
offsets: Vec::with_capacity(text.len() + 1),
|
|
251
|
+
};
|
|
252
|
+
let framed = codec && crate::codec::has_encoding(text);
|
|
253
|
+
crate::codec::visit_units(text, framed, &mut indexed);
|
|
254
|
+
indexed.offsets.push(text.len() as u32);
|
|
255
|
+
Self {
|
|
256
|
+
text,
|
|
257
|
+
units: indexed.units,
|
|
258
|
+
offsets: Some(indexed.offsets),
|
|
259
|
+
codec,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
fn slice(&self, start: usize, end: usize) -> String {
|
|
263
|
+
let start = start.min(self.units.len());
|
|
264
|
+
let end = end.clamp(start, self.units.len());
|
|
265
|
+
let Some(offsets) = self.offsets.as_ref() else {
|
|
266
|
+
return self.text[start..end].to_owned();
|
|
267
|
+
};
|
|
268
|
+
match (offsets[start], offsets[end]) {
|
|
269
|
+
(from, to) if from != u32::MAX && to != u32::MAX => {
|
|
270
|
+
self.text[from as usize..to as usize].to_owned()
|
|
271
|
+
}
|
|
272
|
+
_ if self.codec => crate::codec::from_units(&self.units[start..end]),
|
|
273
|
+
_ => String::from_utf16_lossy(&self.units[start..end]),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Parses markup with the native port of htmlparser2's Tokenizer.
|
|
279
|
+
pub(crate) fn parse_markup(source: &str, options: Value) -> DomArena {
|
|
280
|
+
let source = Source::new(source, &options);
|
|
281
|
+
let decode = options.get("decodeEntities") != Some(&Value::Bool(false));
|
|
282
|
+
let mut builder = Builder::new(&source, options);
|
|
283
|
+
let xml = builder.xml;
|
|
284
|
+
crate::markup_tokenizer::tokenize(&source.units, xml, decode, |token| builder.handle(token));
|
|
285
|
+
builder.arena
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Replays events recorded from a JavaScript htmlparser2 Tokenizer.
|
|
148
289
|
pub(crate) fn parse(source: &str, events: &Value, options: Value) -> DomArena {
|
|
149
|
-
let
|
|
150
|
-
let
|
|
151
|
-
let
|
|
152
|
-
.get("lowerCaseTags")
|
|
153
|
-
.and_then(Value::as_bool)
|
|
154
|
-
.unwrap_or(!xml);
|
|
155
|
-
let lower_attrs = options
|
|
156
|
-
.get("lowerCaseAttributeNames")
|
|
157
|
-
.and_then(Value::as_bool)
|
|
158
|
-
.unwrap_or(!xml);
|
|
159
|
-
let mut builder = Builder {
|
|
160
|
-
arena: DomArena::from_records(vec![DomNode::new("root")], options),
|
|
161
|
-
source: if utf16_codec {
|
|
162
|
-
crate::codec::to_units(source)
|
|
163
|
-
} else {
|
|
164
|
-
source.encode_utf16().collect()
|
|
165
|
-
},
|
|
166
|
-
parents: vec![0],
|
|
167
|
-
foreign: vec![xml],
|
|
168
|
-
pending: None,
|
|
169
|
-
attribute_name: String::new(),
|
|
170
|
-
attribute_value: String::new(),
|
|
171
|
-
start: 0,
|
|
172
|
-
end: 0,
|
|
173
|
-
xml,
|
|
174
|
-
lower_tags,
|
|
175
|
-
};
|
|
290
|
+
let source = Source::new(source, &options);
|
|
291
|
+
let units = source.units.len() as i64;
|
|
292
|
+
let mut builder = Builder::new(&source, options);
|
|
176
293
|
for event in events.as_array().into_iter().flatten() {
|
|
177
294
|
let Some(tuple) = event.as_array() else {
|
|
178
295
|
continue;
|
|
@@ -180,176 +297,232 @@ pub(crate) fn parse(source: &str, events: &Value, options: Value) -> DomArena {
|
|
|
180
297
|
let Some(event) = tuple.first().and_then(Value::as_str) else {
|
|
181
298
|
continue;
|
|
182
299
|
};
|
|
183
|
-
|
|
300
|
+
// htmlparser2 reports an unset section start as -1, which its
|
|
301
|
+
// `slice` resolves from the end of the input.
|
|
302
|
+
let arg = |index: usize| {
|
|
303
|
+
let value = tuple.get(index + 1).and_then(Value::as_i64).unwrap_or(0);
|
|
304
|
+
if value < 0 {
|
|
305
|
+
(units + value).max(0) as usize
|
|
306
|
+
} else {
|
|
307
|
+
value as usize
|
|
308
|
+
}
|
|
309
|
+
};
|
|
184
310
|
let (a, b, c) = (arg(0), arg(1), arg(2));
|
|
185
311
|
match event {
|
|
186
|
-
"ontext" =>
|
|
187
|
-
let text = builder.slice(a, b);
|
|
188
|
-
builder.text(text, b);
|
|
189
|
-
}
|
|
312
|
+
"ontext" => builder.on_text(a, b, false),
|
|
190
313
|
"ontextentity" => {
|
|
191
|
-
let text =
|
|
192
|
-
crate::codec::from_units(&[a as u16])
|
|
193
|
-
} else {
|
|
194
|
-
crate::codec::encode_scalar(char::from_u32(a as u32).unwrap_or('\u{fffd}'))
|
|
195
|
-
};
|
|
314
|
+
let text = builder.code_point(a);
|
|
196
315
|
builder.text(text, b);
|
|
197
316
|
}
|
|
198
|
-
"onopentagname" =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"onopentagend" => {
|
|
204
|
-
builder.end = a;
|
|
205
|
-
builder.end_open(false);
|
|
206
|
-
builder.start = a + 1;
|
|
207
|
-
}
|
|
208
|
-
"onselfclosingtag" => {
|
|
209
|
-
builder.end = a;
|
|
210
|
-
let self_closing = builder
|
|
211
|
-
.arena
|
|
212
|
-
.options
|
|
213
|
-
.get("recognizeSelfClosing")
|
|
214
|
-
.and_then(Value::as_bool)
|
|
215
|
-
.unwrap_or(xml)
|
|
216
|
-
|| *builder.foreign.last().unwrap_or(&xml);
|
|
217
|
-
builder.end_open(self_closing);
|
|
218
|
-
builder.start = a + 1;
|
|
219
|
-
}
|
|
220
|
-
"onattribname" => {
|
|
221
|
-
builder.start = a;
|
|
222
|
-
let name = builder.slice(a, b);
|
|
223
|
-
builder.attribute_name = if lower_attrs {
|
|
224
|
-
name.to_lowercase()
|
|
225
|
-
} else {
|
|
226
|
-
name
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
"onattribdata" => {
|
|
230
|
-
let data = builder.slice(a, b);
|
|
231
|
-
builder.attribute_value.push_str(&data);
|
|
232
|
-
}
|
|
317
|
+
"onopentagname" => builder.on_open_tag_name(a, b),
|
|
318
|
+
"onopentagend" => builder.on_open_tag_end(a),
|
|
319
|
+
"onselfclosingtag" => builder.on_self_closing_tag(a),
|
|
320
|
+
"onattribname" => builder.on_attribute_name(a, b),
|
|
321
|
+
"onattribdata" => builder.on_attribute_data(a, b, false),
|
|
233
322
|
"onattribentity" => {
|
|
234
|
-
builder.
|
|
235
|
-
|
|
236
|
-
} else {
|
|
237
|
-
crate::codec::encode_scalar(char::from_u32(a as u32).unwrap_or('\u{fffd}'))
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
"onattribend" => {
|
|
241
|
-
builder.end = b;
|
|
242
|
-
if let Some(pending) = builder.pending.as_mut() {
|
|
243
|
-
pending
|
|
244
|
-
.attrs
|
|
245
|
-
.entry(builder.attribute_name.clone())
|
|
246
|
-
.or_insert_with(|| Value::String(builder.attribute_value.clone()));
|
|
247
|
-
}
|
|
248
|
-
builder.attribute_value.clear();
|
|
249
|
-
}
|
|
250
|
-
"onclosetag" => {
|
|
251
|
-
builder.end = b;
|
|
252
|
-
let name = builder.name(a, b);
|
|
253
|
-
if !xml
|
|
254
|
-
&& matches!(
|
|
255
|
-
name.as_str(),
|
|
256
|
-
"svg"
|
|
257
|
-
| "math"
|
|
258
|
-
| "mi"
|
|
259
|
-
| "mo"
|
|
260
|
-
| "mn"
|
|
261
|
-
| "ms"
|
|
262
|
-
| "mtext"
|
|
263
|
-
| "annotation-xml"
|
|
264
|
-
| "foreignobject"
|
|
265
|
-
| "desc"
|
|
266
|
-
| "title"
|
|
267
|
-
)
|
|
268
|
-
{
|
|
269
|
-
builder.foreign.pop();
|
|
270
|
-
}
|
|
271
|
-
if !(!xml && html_parser_void(&name)) {
|
|
272
|
-
if let Some(position) = builder.parents.iter().rposition(|id| {
|
|
273
|
-
builder
|
|
274
|
-
.arena
|
|
275
|
-
.node(*id)
|
|
276
|
-
.and_then(|node| node.name.as_deref())
|
|
277
|
-
== Some(name.as_str())
|
|
278
|
-
}) {
|
|
279
|
-
while builder.parents.len() > position {
|
|
280
|
-
builder.close();
|
|
281
|
-
}
|
|
282
|
-
} else if !xml && name == "p" {
|
|
283
|
-
builder.open(name);
|
|
284
|
-
builder.end_open(true);
|
|
285
|
-
}
|
|
286
|
-
} else if !xml && name == "br" {
|
|
287
|
-
builder.open(name);
|
|
288
|
-
builder.end_open(true);
|
|
289
|
-
}
|
|
290
|
-
builder.start = b + 1;
|
|
323
|
+
let text = builder.code_point(a);
|
|
324
|
+
builder.attribute_value.push_str(&text);
|
|
291
325
|
}
|
|
326
|
+
"onattribend" => builder.on_attribute_end(b),
|
|
327
|
+
"onclosetag" => builder.on_close_tag(a, b),
|
|
292
328
|
"ondeclaration" => builder.instruction(a, b, '!'),
|
|
293
329
|
"onprocessinginstruction" => builder.instruction(a, b, '?'),
|
|
294
|
-
"oncomment" =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
330
|
+
"oncomment" => builder.on_comment(a, b, c),
|
|
331
|
+
"oncdata" => builder.on_cdata(a, b, c),
|
|
332
|
+
"onend" => builder.on_end(),
|
|
333
|
+
_ => {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
builder.arena
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
impl Builder<'_> {
|
|
340
|
+
fn handle(&mut self, token: Token) {
|
|
341
|
+
match token {
|
|
342
|
+
Token::Text { start, end, decode } => self.on_text(start, end, decode),
|
|
343
|
+
Token::OpenTagName { start, end } => self.on_open_tag_name(start, end),
|
|
344
|
+
Token::OpenTagEnd { end } => self.on_open_tag_end(end),
|
|
345
|
+
Token::SelfClosingTag { end } => self.on_self_closing_tag(end),
|
|
346
|
+
Token::AttributeName { start, end } => self.on_attribute_name(start, end),
|
|
347
|
+
Token::AttributeData { start, end, decode } => {
|
|
348
|
+
self.on_attribute_data(start, end, decode)
|
|
306
349
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
Token::AttributeEnd { end } => self.on_attribute_end(end),
|
|
351
|
+
Token::CloseTag { start, end } => self.on_close_tag(start, end),
|
|
352
|
+
Token::Comment {
|
|
353
|
+
start,
|
|
354
|
+
end,
|
|
355
|
+
end_offset,
|
|
356
|
+
} => self.on_comment(start, end, end_offset),
|
|
357
|
+
Token::Cdata {
|
|
358
|
+
start,
|
|
359
|
+
end,
|
|
360
|
+
end_offset,
|
|
361
|
+
} => self.on_cdata(start, end, end_offset),
|
|
362
|
+
Token::Declaration { start, end } => self.instruction(start, end, '!'),
|
|
363
|
+
Token::ProcessingInstruction { start, end } => self.instruction(start, end, '?'),
|
|
364
|
+
Token::End => self.on_end(),
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
fn code_point(&self, code_point: usize) -> String {
|
|
368
|
+
if code_point <= 0xffff {
|
|
369
|
+
crate::codec::from_units(&[code_point as u16])
|
|
370
|
+
} else {
|
|
371
|
+
crate::codec::encode_scalar(char::from_u32(code_point as u32).unwrap_or('\u{fffd}'))
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
fn on_text(&mut self, start: usize, end: usize, decode: bool) {
|
|
375
|
+
let raw = self.slice(start, end);
|
|
376
|
+
let text = if decode && raw.contains('&') {
|
|
377
|
+
crate::arena::decode_entities_mode(&raw, self.xml, false, self.source.codec)
|
|
378
|
+
} else {
|
|
379
|
+
raw
|
|
380
|
+
};
|
|
381
|
+
self.text(text, end);
|
|
382
|
+
}
|
|
383
|
+
fn on_open_tag_name(&mut self, start: usize, end: usize) {
|
|
384
|
+
self.end = end;
|
|
385
|
+
let name = self.name(start, end);
|
|
386
|
+
self.open(name);
|
|
387
|
+
}
|
|
388
|
+
fn on_open_tag_end(&mut self, end: usize) {
|
|
389
|
+
self.end = end;
|
|
390
|
+
self.end_open(false);
|
|
391
|
+
self.start = end + 1;
|
|
392
|
+
}
|
|
393
|
+
fn on_self_closing_tag(&mut self, end: usize) {
|
|
394
|
+
self.end = end;
|
|
395
|
+
let self_closing = self.recognize_self_closing.unwrap_or(self.xml)
|
|
396
|
+
|| *self.foreign.last().unwrap_or(&self.xml);
|
|
397
|
+
self.end_open(self_closing);
|
|
398
|
+
self.start = end + 1;
|
|
399
|
+
}
|
|
400
|
+
fn on_attribute_name(&mut self, start: usize, end: usize) {
|
|
401
|
+
self.start = start;
|
|
402
|
+
let name = self.slice(start, end);
|
|
403
|
+
self.attribute_name = if self.lower_attrs {
|
|
404
|
+
lowercase(name)
|
|
405
|
+
} else {
|
|
406
|
+
name
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
fn on_attribute_data(&mut self, start: usize, end: usize, decode: bool) {
|
|
410
|
+
let raw = self.slice(start, end);
|
|
411
|
+
let data = if decode && raw.contains('&') {
|
|
412
|
+
crate::arena::decode_entities_mode(&raw, self.xml, true, self.source.codec)
|
|
413
|
+
} else {
|
|
414
|
+
raw
|
|
415
|
+
};
|
|
416
|
+
if self.attribute_value.is_empty() {
|
|
417
|
+
self.attribute_value = data;
|
|
418
|
+
} else {
|
|
419
|
+
self.attribute_value.push_str(&data);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
fn on_attribute_end(&mut self, end: usize) {
|
|
423
|
+
self.end = end;
|
|
424
|
+
let value = std::mem::take(&mut self.attribute_value);
|
|
425
|
+
if let Some(pending) = self.pending.as_mut() {
|
|
426
|
+
if !pending.attrs.contains_key(&self.attribute_name) {
|
|
427
|
+
pending
|
|
428
|
+
.attrs
|
|
429
|
+
.insert(self.attribute_name.clone(), Value::String(value));
|
|
344
430
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
fn on_close_tag(&mut self, start: usize, end: usize) {
|
|
434
|
+
self.end = end;
|
|
435
|
+
let name = self.name(start, end);
|
|
436
|
+
if !self.xml
|
|
437
|
+
&& matches!(
|
|
438
|
+
name.as_str(),
|
|
439
|
+
"svg"
|
|
440
|
+
| "math"
|
|
441
|
+
| "mi"
|
|
442
|
+
| "mo"
|
|
443
|
+
| "mn"
|
|
444
|
+
| "ms"
|
|
445
|
+
| "mtext"
|
|
446
|
+
| "annotation-xml"
|
|
447
|
+
| "foreignobject"
|
|
448
|
+
| "desc"
|
|
449
|
+
| "title"
|
|
450
|
+
)
|
|
451
|
+
{
|
|
452
|
+
self.foreign.pop();
|
|
453
|
+
}
|
|
454
|
+
if !(!self.xml && html_parser_void(&name)) {
|
|
455
|
+
if let Some(position) = self.parents.iter().rposition(|id| {
|
|
456
|
+
self.arena.node(*id).and_then(|node| node.name.as_deref()) == Some(name.as_str())
|
|
457
|
+
}) {
|
|
458
|
+
while self.parents.len() > position {
|
|
459
|
+
self.close();
|
|
349
460
|
}
|
|
461
|
+
} else if !self.xml && name == "p" {
|
|
462
|
+
self.open(name);
|
|
463
|
+
self.end_open(true);
|
|
350
464
|
}
|
|
351
|
-
|
|
465
|
+
} else if !self.xml && name == "br" {
|
|
466
|
+
self.open(name);
|
|
467
|
+
self.end_open(true);
|
|
468
|
+
}
|
|
469
|
+
self.start = end + 1;
|
|
470
|
+
}
|
|
471
|
+
fn on_comment(&mut self, start: usize, end: usize, end_offset: usize) {
|
|
472
|
+
let mut node = DomNode::new("comment");
|
|
473
|
+
node.data = Some(self.slice(start, end.saturating_sub(end_offset)));
|
|
474
|
+
append_parsed(
|
|
475
|
+
&mut self.arena,
|
|
476
|
+
*self.parents.last().unwrap(),
|
|
477
|
+
node,
|
|
478
|
+
self.start,
|
|
479
|
+
end,
|
|
480
|
+
);
|
|
481
|
+
self.end = end;
|
|
482
|
+
self.start = end + 1;
|
|
483
|
+
}
|
|
484
|
+
fn on_cdata(&mut self, start: usize, end: usize, end_offset: usize) {
|
|
485
|
+
let data = self.slice(start, end.saturating_sub(end_offset));
|
|
486
|
+
if self.xml || self.arena.options.get("recognizeCDATA") == Some(&Value::Bool(true)) {
|
|
487
|
+
let id = append_parsed(
|
|
488
|
+
&mut self.arena,
|
|
489
|
+
*self.parents.last().unwrap(),
|
|
490
|
+
DomNode::new("cdata"),
|
|
491
|
+
self.start,
|
|
492
|
+
end,
|
|
493
|
+
);
|
|
494
|
+
let mut text = DomNode::new("text");
|
|
495
|
+
text.data = Some(data);
|
|
496
|
+
text.parent = Some(id);
|
|
497
|
+
text.extras.insert("startIndex".into(), Value::Null);
|
|
498
|
+
text.extras.insert(
|
|
499
|
+
"endIndex".into(),
|
|
500
|
+
if self.arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
501
|
+
json!(end)
|
|
502
|
+
} else {
|
|
503
|
+
Value::Null
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
let child = self.arena.push(text);
|
|
507
|
+
self.arena.nodes[id as usize].children.push(child);
|
|
508
|
+
} else {
|
|
509
|
+
let mut node = DomNode::new("comment");
|
|
510
|
+
node.data = Some(format!("[CDATA[{data}]]"));
|
|
511
|
+
append_parsed(
|
|
512
|
+
&mut self.arena,
|
|
513
|
+
*self.parents.last().unwrap(),
|
|
514
|
+
node,
|
|
515
|
+
self.start,
|
|
516
|
+
end,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
self.end = end;
|
|
520
|
+
self.start = end + 1;
|
|
521
|
+
}
|
|
522
|
+
fn on_end(&mut self) {
|
|
523
|
+
self.end = self.start;
|
|
524
|
+
while self.parents.len() > 1 {
|
|
525
|
+
self.close();
|
|
352
526
|
}
|
|
353
527
|
}
|
|
354
|
-
builder.arena
|
|
355
528
|
}
|
package/native/vendor/README.md
CHANGED
|
@@ -13,8 +13,9 @@ observe the same construction process used by the Rust HTML5 tree builder.
|
|
|
13
13
|
Ordinary parsing uses the default observer methods.
|
|
14
14
|
|
|
15
15
|
An opt-in tokenizer flag also escapes private-use characters produced by
|
|
16
|
-
character references
|
|
17
|
-
|
|
16
|
+
character references, emitting each escape and its character as one text token.
|
|
17
|
+
This preserves the distinction between literal Unicode and Rusteerio's internal
|
|
18
|
+
representation of lone JavaScript UTF-16 surrogates.
|
|
18
19
|
The flag is disabled for upstream users of these crates. The original HTML5
|
|
19
20
|
character-reference decoding rules remain in place.
|
|
20
21
|
|