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/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,22 +22,41 @@ struct Builder {
|
|
|
20
22
|
end: usize,
|
|
21
23
|
xml: bool,
|
|
22
24
|
lower_tags: bool,
|
|
25
|
+
lower_attrs: bool,
|
|
23
26
|
}
|
|
24
|
-
impl Builder {
|
|
27
|
+
impl<'a> Builder<'a> {
|
|
28
|
+
fn new(source: &'a Source<'a>, options: Value) -> Self {
|
|
29
|
+
let xml = crate::arena::xml_mode(&options);
|
|
30
|
+
let lower_tags = options
|
|
31
|
+
.get("lowerCaseTags")
|
|
32
|
+
.and_then(Value::as_bool)
|
|
33
|
+
.unwrap_or(!xml);
|
|
34
|
+
let lower_attrs = options
|
|
35
|
+
.get("lowerCaseAttributeNames")
|
|
36
|
+
.and_then(Value::as_bool)
|
|
37
|
+
.unwrap_or(!xml);
|
|
38
|
+
Self {
|
|
39
|
+
arena: DomArena::from_records(vec![DomNode::new("root")], options),
|
|
40
|
+
source,
|
|
41
|
+
parents: vec![0],
|
|
42
|
+
foreign: vec![xml],
|
|
43
|
+
pending: None,
|
|
44
|
+
attribute_name: String::new(),
|
|
45
|
+
attribute_value: String::new(),
|
|
46
|
+
start: 0,
|
|
47
|
+
end: 0,
|
|
48
|
+
xml,
|
|
49
|
+
lower_tags,
|
|
50
|
+
lower_attrs,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
25
53
|
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
|
-
)
|
|
54
|
+
self.source.slice(start, end)
|
|
34
55
|
}
|
|
35
56
|
fn name(&self, start: usize, end: usize) -> String {
|
|
36
57
|
let name = self.slice(start, end);
|
|
37
58
|
if self.lower_tags {
|
|
38
|
-
name
|
|
59
|
+
lowercase(name)
|
|
39
60
|
} else {
|
|
40
61
|
name
|
|
41
62
|
}
|
|
@@ -61,7 +82,8 @@ impl Builder {
|
|
|
61
82
|
self.close();
|
|
62
83
|
}
|
|
63
84
|
}
|
|
64
|
-
|
|
85
|
+
// htmlparser2 tracks foreign content only in HTML mode.
|
|
86
|
+
if !self.xml && !html_parser_void(&name) {
|
|
65
87
|
if matches!(name.as_str(), "svg" | "math") {
|
|
66
88
|
self.foreign.push(true);
|
|
67
89
|
} else if matches!(
|
|
@@ -145,34 +167,107 @@ impl Builder {
|
|
|
145
167
|
}
|
|
146
168
|
}
|
|
147
169
|
|
|
170
|
+
/// Sources from JavaScript, or that contain codec frames, use the lossless
|
|
171
|
+
/// UTF-16 representation; others are plain Unicode.
|
|
172
|
+
fn is_codec_encoded(options: &Value) -> bool {
|
|
173
|
+
options.get("__utf16Codec") == Some(&Value::Bool(true))
|
|
174
|
+
|| options.get("__losslessUnicode") == Some(&Value::Bool(true))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn lowercase(mut name: String) -> String {
|
|
178
|
+
if name.is_ascii() {
|
|
179
|
+
name.make_ascii_lowercase();
|
|
180
|
+
name
|
|
181
|
+
} else {
|
|
182
|
+
name.to_lowercase()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// The source as UTF-16 units, with each unit's byte offset in the original
|
|
187
|
+
/// string so spans can be copied from it instead of re-encoded.
|
|
188
|
+
struct Source<'a> {
|
|
189
|
+
text: &'a str,
|
|
190
|
+
units: Vec<u16>,
|
|
191
|
+
/// Byte offset per unit index, plus one for the end; `u32::MAX` marks an
|
|
192
|
+
/// index inside a character. `None` for ASCII, where offsets are indices.
|
|
193
|
+
offsets: Option<Vec<u32>>,
|
|
194
|
+
codec: bool,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
struct IndexedUnits {
|
|
198
|
+
units: Vec<u16>,
|
|
199
|
+
offsets: Vec<u32>,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
impl crate::codec::UnitSink for IndexedUnits {
|
|
203
|
+
fn ascii(&mut self, start: usize, run: &[u8]) {
|
|
204
|
+
self.units.extend(run.iter().map(|value| u16::from(*value)));
|
|
205
|
+
self.offsets
|
|
206
|
+
.extend((start..start + run.len()).map(|offset| offset as u32));
|
|
207
|
+
}
|
|
208
|
+
fn unit(&mut self, offset: Option<usize>, unit: u16) {
|
|
209
|
+
self.units.push(unit);
|
|
210
|
+
self.offsets
|
|
211
|
+
.push(offset.map_or(u32::MAX, |offset| offset as u32));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
impl<'a> Source<'a> {
|
|
216
|
+
fn new(text: &'a str, options: &Value) -> Self {
|
|
217
|
+
let codec = is_codec_encoded(options);
|
|
218
|
+
if text.is_ascii() {
|
|
219
|
+
return Self {
|
|
220
|
+
text,
|
|
221
|
+
units: text.bytes().map(u16::from).collect(),
|
|
222
|
+
offsets: None,
|
|
223
|
+
codec,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
let mut indexed = IndexedUnits {
|
|
227
|
+
units: Vec::with_capacity(text.len()),
|
|
228
|
+
offsets: Vec::with_capacity(text.len() + 1),
|
|
229
|
+
};
|
|
230
|
+
let framed = codec && crate::codec::has_encoding(text);
|
|
231
|
+
crate::codec::visit_units(text, framed, &mut indexed);
|
|
232
|
+
indexed.offsets.push(text.len() as u32);
|
|
233
|
+
Self {
|
|
234
|
+
text,
|
|
235
|
+
units: indexed.units,
|
|
236
|
+
offsets: Some(indexed.offsets),
|
|
237
|
+
codec,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
fn slice(&self, start: usize, end: usize) -> String {
|
|
241
|
+
let start = start.min(self.units.len());
|
|
242
|
+
let end = end.clamp(start, self.units.len());
|
|
243
|
+
let Some(offsets) = self.offsets.as_ref() else {
|
|
244
|
+
return self.text[start..end].to_owned();
|
|
245
|
+
};
|
|
246
|
+
match (offsets[start], offsets[end]) {
|
|
247
|
+
(from, to) if from != u32::MAX && to != u32::MAX => {
|
|
248
|
+
self.text[from as usize..to as usize].to_owned()
|
|
249
|
+
}
|
|
250
|
+
_ if self.codec => crate::codec::from_units(&self.units[start..end]),
|
|
251
|
+
_ => String::from_utf16_lossy(&self.units[start..end]),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Parses markup with the native port of htmlparser2's Tokenizer.
|
|
257
|
+
pub(crate) fn parse_markup(source: &str, options: Value) -> DomArena {
|
|
258
|
+
let source = Source::new(source, &options);
|
|
259
|
+
let decode = options.get("decodeEntities") != Some(&Value::Bool(false));
|
|
260
|
+
let mut builder = Builder::new(&source, options);
|
|
261
|
+
let xml = builder.xml;
|
|
262
|
+
crate::markup_tokenizer::tokenize(&source.units, xml, decode, |token| builder.handle(token));
|
|
263
|
+
builder.arena
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Replays events recorded from a JavaScript htmlparser2 Tokenizer.
|
|
148
267
|
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
|
-
};
|
|
268
|
+
let source = Source::new(source, &options);
|
|
269
|
+
let units = source.units.len() as i64;
|
|
270
|
+
let mut builder = Builder::new(&source, options);
|
|
176
271
|
for event in events.as_array().into_iter().flatten() {
|
|
177
272
|
let Some(tuple) = event.as_array() else {
|
|
178
273
|
continue;
|
|
@@ -180,176 +275,237 @@ pub(crate) fn parse(source: &str, events: &Value, options: Value) -> DomArena {
|
|
|
180
275
|
let Some(event) = tuple.first().and_then(Value::as_str) else {
|
|
181
276
|
continue;
|
|
182
277
|
};
|
|
183
|
-
|
|
278
|
+
// htmlparser2 reports an unset section start as -1, which its
|
|
279
|
+
// `slice` resolves from the end of the input.
|
|
280
|
+
let arg = |index: usize| {
|
|
281
|
+
let value = tuple.get(index + 1).and_then(Value::as_i64).unwrap_or(0);
|
|
282
|
+
if value < 0 {
|
|
283
|
+
(units + value).max(0) as usize
|
|
284
|
+
} else {
|
|
285
|
+
value as usize
|
|
286
|
+
}
|
|
287
|
+
};
|
|
184
288
|
let (a, b, c) = (arg(0), arg(1), arg(2));
|
|
185
289
|
match event {
|
|
186
|
-
"ontext" =>
|
|
187
|
-
let text = builder.slice(a, b);
|
|
188
|
-
builder.text(text, b);
|
|
189
|
-
}
|
|
290
|
+
"ontext" => builder.on_text(a, b, false),
|
|
190
291
|
"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
|
-
};
|
|
292
|
+
let text = builder.code_point(a);
|
|
196
293
|
builder.text(text, b);
|
|
197
294
|
}
|
|
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
|
-
}
|
|
295
|
+
"onopentagname" => builder.on_open_tag_name(a, b),
|
|
296
|
+
"onopentagend" => builder.on_open_tag_end(a),
|
|
297
|
+
"onselfclosingtag" => builder.on_self_closing_tag(a),
|
|
298
|
+
"onattribname" => builder.on_attribute_name(a, b),
|
|
299
|
+
"onattribdata" => builder.on_attribute_data(a, b, false),
|
|
233
300
|
"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;
|
|
301
|
+
let text = builder.code_point(a);
|
|
302
|
+
builder.attribute_value.push_str(&text);
|
|
291
303
|
}
|
|
304
|
+
"onattribend" => builder.on_attribute_end(b),
|
|
305
|
+
"onclosetag" => builder.on_close_tag(a, b),
|
|
292
306
|
"ondeclaration" => builder.instruction(a, b, '!'),
|
|
293
307
|
"onprocessinginstruction" => builder.instruction(a, b, '?'),
|
|
294
|
-
"oncomment" =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
"oncomment" => builder.on_comment(a, b, c),
|
|
309
|
+
"oncdata" => builder.on_cdata(a, b, c),
|
|
310
|
+
"onend" => builder.on_end(),
|
|
311
|
+
_ => {}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
builder.arena
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
impl Builder<'_> {
|
|
318
|
+
fn handle(&mut self, token: Token) {
|
|
319
|
+
match token {
|
|
320
|
+
Token::Text { start, end, decode } => self.on_text(start, end, decode),
|
|
321
|
+
Token::OpenTagName { start, end } => self.on_open_tag_name(start, end),
|
|
322
|
+
Token::OpenTagEnd { end } => self.on_open_tag_end(end),
|
|
323
|
+
Token::SelfClosingTag { end } => self.on_self_closing_tag(end),
|
|
324
|
+
Token::AttributeName { start, end } => self.on_attribute_name(start, end),
|
|
325
|
+
Token::AttributeData { start, end, decode } => {
|
|
326
|
+
self.on_attribute_data(start, end, decode)
|
|
306
327
|
}
|
|
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
|
-
|
|
328
|
+
Token::AttributeEnd { end } => self.on_attribute_end(end),
|
|
329
|
+
Token::CloseTag { start, end } => self.on_close_tag(start, end),
|
|
330
|
+
Token::Comment {
|
|
331
|
+
start,
|
|
332
|
+
end,
|
|
333
|
+
end_offset,
|
|
334
|
+
} => self.on_comment(start, end, end_offset),
|
|
335
|
+
Token::Cdata {
|
|
336
|
+
start,
|
|
337
|
+
end,
|
|
338
|
+
end_offset,
|
|
339
|
+
} => self.on_cdata(start, end, end_offset),
|
|
340
|
+
Token::Declaration { start, end } => self.instruction(start, end, '!'),
|
|
341
|
+
Token::ProcessingInstruction { start, end } => self.instruction(start, end, '?'),
|
|
342
|
+
Token::End => self.on_end(),
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
fn code_point(&self, code_point: usize) -> String {
|
|
346
|
+
if code_point <= 0xffff {
|
|
347
|
+
crate::codec::from_units(&[code_point as u16])
|
|
348
|
+
} else {
|
|
349
|
+
crate::codec::encode_scalar(char::from_u32(code_point as u32).unwrap_or('\u{fffd}'))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
fn on_text(&mut self, start: usize, end: usize, decode: bool) {
|
|
353
|
+
let raw = self.slice(start, end);
|
|
354
|
+
let text = if decode && raw.contains('&') {
|
|
355
|
+
crate::arena::decode_entities_mode(&raw, self.xml, false, self.source.codec)
|
|
356
|
+
} else {
|
|
357
|
+
raw
|
|
358
|
+
};
|
|
359
|
+
self.text(text, end);
|
|
360
|
+
}
|
|
361
|
+
fn on_open_tag_name(&mut self, start: usize, end: usize) {
|
|
362
|
+
self.end = end;
|
|
363
|
+
let name = self.name(start, end);
|
|
364
|
+
self.open(name);
|
|
365
|
+
}
|
|
366
|
+
fn on_open_tag_end(&mut self, end: usize) {
|
|
367
|
+
self.end = end;
|
|
368
|
+
self.end_open(false);
|
|
369
|
+
self.start = end + 1;
|
|
370
|
+
}
|
|
371
|
+
fn on_self_closing_tag(&mut self, end: usize) {
|
|
372
|
+
self.end = end;
|
|
373
|
+
let self_closing = self
|
|
374
|
+
.arena
|
|
375
|
+
.options
|
|
376
|
+
.get("recognizeSelfClosing")
|
|
377
|
+
.and_then(Value::as_bool)
|
|
378
|
+
.unwrap_or(self.xml)
|
|
379
|
+
|| *self.foreign.last().unwrap_or(&self.xml);
|
|
380
|
+
self.end_open(self_closing);
|
|
381
|
+
self.start = end + 1;
|
|
382
|
+
}
|
|
383
|
+
fn on_attribute_name(&mut self, start: usize, end: usize) {
|
|
384
|
+
self.start = start;
|
|
385
|
+
let name = self.slice(start, end);
|
|
386
|
+
self.attribute_name = if self.lower_attrs {
|
|
387
|
+
lowercase(name)
|
|
388
|
+
} else {
|
|
389
|
+
name
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
fn on_attribute_data(&mut self, start: usize, end: usize, decode: bool) {
|
|
393
|
+
let raw = self.slice(start, end);
|
|
394
|
+
let data = if decode && raw.contains('&') {
|
|
395
|
+
crate::arena::decode_entities_mode(&raw, self.xml, true, self.source.codec)
|
|
396
|
+
} else {
|
|
397
|
+
raw
|
|
398
|
+
};
|
|
399
|
+
if self.attribute_value.is_empty() {
|
|
400
|
+
self.attribute_value = data;
|
|
401
|
+
} else {
|
|
402
|
+
self.attribute_value.push_str(&data);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
fn on_attribute_end(&mut self, end: usize) {
|
|
406
|
+
self.end = end;
|
|
407
|
+
let value = std::mem::take(&mut self.attribute_value);
|
|
408
|
+
if let Some(pending) = self.pending.as_mut() {
|
|
409
|
+
if !pending.attrs.contains_key(&self.attribute_name) {
|
|
410
|
+
pending
|
|
411
|
+
.attrs
|
|
412
|
+
.insert(self.attribute_name.clone(), Value::String(value));
|
|
344
413
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
fn on_close_tag(&mut self, start: usize, end: usize) {
|
|
417
|
+
self.end = end;
|
|
418
|
+
let name = self.name(start, end);
|
|
419
|
+
if !self.xml
|
|
420
|
+
&& matches!(
|
|
421
|
+
name.as_str(),
|
|
422
|
+
"svg"
|
|
423
|
+
| "math"
|
|
424
|
+
| "mi"
|
|
425
|
+
| "mo"
|
|
426
|
+
| "mn"
|
|
427
|
+
| "ms"
|
|
428
|
+
| "mtext"
|
|
429
|
+
| "annotation-xml"
|
|
430
|
+
| "foreignobject"
|
|
431
|
+
| "desc"
|
|
432
|
+
| "title"
|
|
433
|
+
)
|
|
434
|
+
{
|
|
435
|
+
self.foreign.pop();
|
|
436
|
+
}
|
|
437
|
+
if !(!self.xml && html_parser_void(&name)) {
|
|
438
|
+
if let Some(position) = self.parents.iter().rposition(|id| {
|
|
439
|
+
self.arena.node(*id).and_then(|node| node.name.as_deref()) == Some(name.as_str())
|
|
440
|
+
}) {
|
|
441
|
+
while self.parents.len() > position {
|
|
442
|
+
self.close();
|
|
349
443
|
}
|
|
444
|
+
} else if !self.xml && name == "p" {
|
|
445
|
+
self.open(name);
|
|
446
|
+
self.end_open(true);
|
|
350
447
|
}
|
|
351
|
-
|
|
448
|
+
} else if !self.xml && name == "br" {
|
|
449
|
+
self.open(name);
|
|
450
|
+
self.end_open(true);
|
|
451
|
+
}
|
|
452
|
+
self.start = end + 1;
|
|
453
|
+
}
|
|
454
|
+
fn on_comment(&mut self, start: usize, end: usize, end_offset: usize) {
|
|
455
|
+
let mut node = DomNode::new("comment");
|
|
456
|
+
node.data = Some(self.slice(start, end.saturating_sub(end_offset)));
|
|
457
|
+
append_parsed(
|
|
458
|
+
&mut self.arena,
|
|
459
|
+
*self.parents.last().unwrap(),
|
|
460
|
+
node,
|
|
461
|
+
self.start,
|
|
462
|
+
end,
|
|
463
|
+
);
|
|
464
|
+
self.end = end;
|
|
465
|
+
self.start = end + 1;
|
|
466
|
+
}
|
|
467
|
+
fn on_cdata(&mut self, start: usize, end: usize, end_offset: usize) {
|
|
468
|
+
let data = self.slice(start, end.saturating_sub(end_offset));
|
|
469
|
+
if self.xml || self.arena.options.get("recognizeCDATA") == Some(&Value::Bool(true)) {
|
|
470
|
+
let id = append_parsed(
|
|
471
|
+
&mut self.arena,
|
|
472
|
+
*self.parents.last().unwrap(),
|
|
473
|
+
DomNode::new("cdata"),
|
|
474
|
+
self.start,
|
|
475
|
+
end,
|
|
476
|
+
);
|
|
477
|
+
let mut text = DomNode::new("text");
|
|
478
|
+
text.data = Some(data);
|
|
479
|
+
text.parent = Some(id);
|
|
480
|
+
text.extras.insert("startIndex".into(), Value::Null);
|
|
481
|
+
text.extras.insert(
|
|
482
|
+
"endIndex".into(),
|
|
483
|
+
if self.arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
484
|
+
json!(end)
|
|
485
|
+
} else {
|
|
486
|
+
Value::Null
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
let child = self.arena.push(text);
|
|
490
|
+
self.arena.nodes[id as usize].children.push(child);
|
|
491
|
+
} else {
|
|
492
|
+
let mut node = DomNode::new("comment");
|
|
493
|
+
node.data = Some(format!("[CDATA[{data}]]"));
|
|
494
|
+
append_parsed(
|
|
495
|
+
&mut self.arena,
|
|
496
|
+
*self.parents.last().unwrap(),
|
|
497
|
+
node,
|
|
498
|
+
self.start,
|
|
499
|
+
end,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
self.end = end;
|
|
503
|
+
self.start = end + 1;
|
|
504
|
+
}
|
|
505
|
+
fn on_end(&mut self) {
|
|
506
|
+
self.end = self.start;
|
|
507
|
+
while self.parents.len() > 1 {
|
|
508
|
+
self.close();
|
|
352
509
|
}
|
|
353
510
|
}
|
|
354
|
-
builder.arena
|
|
355
511
|
}
|
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
|
|
|
@@ -1698,19 +1698,29 @@ impl<Sink: TokenSink> Tokenizer<Sink> {
|
|
|
1698
1698
|
|
|
1699
1699
|
for i in 0..num_chars {
|
|
1700
1700
|
let c = chars[i as usize];
|
|
1701
|
-
let
|
|
1702
|
-
.
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1701
|
+
let is_escaped =
|
|
1702
|
+
self.opts.rusteerio_codec && ('\u{e000}'..='\u{e800}').contains(&c);
|
|
1703
|
+
match self.state.get() {
|
|
1704
|
+
// The escape and its character form one text token, so a sink
|
|
1705
|
+
// that forwards each token never receives them separately.
|
|
1706
|
+
states::Data | states::RawData(states::Rcdata) if is_escaped => {
|
|
1707
|
+
let mut text = StrTendril::from_char('\u{e800}');
|
|
1708
|
+
text.push_char(c);
|
|
1709
|
+
self.emit_chars(text);
|
|
1710
|
+
},
|
|
1711
|
+
states::Data | states::RawData(states::Rcdata) => self.emit_char(c),
|
|
1712
|
+
|
|
1713
|
+
states::AttributeValue(_) => {
|
|
1714
|
+
if is_escaped {
|
|
1715
|
+
go!(self: push_value '\u{e800}');
|
|
1716
|
+
}
|
|
1717
|
+
go!(self: push_value c);
|
|
1718
|
+
},
|
|
1719
|
+
|
|
1720
|
+
_ => panic!(
|
|
1721
|
+
"state {:?} should not be reachable in process_char_ref",
|
|
1722
|
+
self.state.get()
|
|
1723
|
+
),
|
|
1714
1724
|
}
|
|
1715
1725
|
}
|
|
1716
1726
|
}
|