rusteerio 1.0.1 → 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 +107 -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 +219 -80
- package/native/src/attributes.rs +11 -5
- 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/id_hash.rs +42 -0
- package/native/src/immutable.rs +256 -0
- package/native/src/lib.rs +1039 -171
- 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 +29 -12
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
- 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/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
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
//! A compact copy of every node record that JavaScript decodes lazily. One call
|
|
2
|
+
//! replaces a native round trip per node when a document is read node by node.
|
|
3
|
+
//!
|
|
4
|
+
//! Each node occupies `STRIDE` entries of `nodes`:
|
|
5
|
+
//! 0 type, 1 parent + 1, 2 previous sibling + 1, 3 next sibling + 1,
|
|
6
|
+
//! 4 first entry in `children`, 5 child count, 6 name + 1, 7 data + 1,
|
|
7
|
+
//! 8 first pair in `attributes`, 9 attribute count, 10 flags, 11 start index,
|
|
8
|
+
//! 12 end index, 13 namespace, 14 remaining fields as JSON + 1.
|
|
9
|
+
//! String references index pairs of UTF-16 offsets into `text`.
|
|
10
|
+
//!
|
|
11
|
+
//! JavaScript receives one `Uint32Array`: the lengths of `nodes`, `children`,
|
|
12
|
+
//! `attributes` and `strings`, whether any string holds codec escapes, then
|
|
13
|
+
//! the four lists. String offsets count UTF-16 units of the encoded text, so
|
|
14
|
+
//! JavaScript decodes only the strings it reads. Its memory belongs to V8,
|
|
15
|
+
//! which frees it as soon as the snapshot is collected; an external buffer
|
|
16
|
+
//! would add finalizer bookkeeping and wait for a later event-loop turn.
|
|
17
|
+
use html5ever::{LocalName, Namespace};
|
|
18
|
+
use napi::{Env, JsObject, JsTypedArray, Result, TypedArrayType};
|
|
19
|
+
use scraper::Node;
|
|
20
|
+
use serde_json::{Map, Value};
|
|
21
|
+
|
|
22
|
+
use crate::arena::DomNode;
|
|
23
|
+
use crate::id_hash::IdMap;
|
|
24
|
+
use crate::NativeDocument;
|
|
25
|
+
|
|
26
|
+
pub(crate) const STRIDE: usize = 15;
|
|
27
|
+
/// Text longer than this stays out of snapshots: such nodes, mostly inline
|
|
28
|
+
/// scripts and styles, are read individually if JavaScript asks for them.
|
|
29
|
+
const LARGE_TEXT_BYTES: usize = 4096;
|
|
30
|
+
const ABSENT: u32 = u32::MAX;
|
|
31
|
+
const HAS_START_INDEX: u32 = 1;
|
|
32
|
+
const HAS_END_INDEX: u32 = 2;
|
|
33
|
+
const HAS_NAMESPACE: u32 = 4;
|
|
34
|
+
const EMPTY_ATTRIBUTE_MAPS: u32 = 8;
|
|
35
|
+
/// The record cannot be represented here; JavaScript reads it individually.
|
|
36
|
+
const OPAQUE: u32 = 16;
|
|
37
|
+
const RELEASED: u32 = 32;
|
|
38
|
+
|
|
39
|
+
pub(crate) struct NodeSnapshot {
|
|
40
|
+
is_encoded: bool,
|
|
41
|
+
nodes: Vec<u32>,
|
|
42
|
+
children: Vec<u32>,
|
|
43
|
+
attributes: Vec<u32>,
|
|
44
|
+
strings: Vec<u32>,
|
|
45
|
+
text: String,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl NodeSnapshot {
|
|
49
|
+
pub(crate) fn into_js(self, env: &Env) -> Result<JsObject> {
|
|
50
|
+
let parts = [&self.nodes, &self.children, &self.attributes, &self.strings];
|
|
51
|
+
let lengths = parts.map(|part| part.len() as u32);
|
|
52
|
+
let header = [
|
|
53
|
+
lengths[0],
|
|
54
|
+
lengths[1],
|
|
55
|
+
lengths[2],
|
|
56
|
+
lengths[3],
|
|
57
|
+
u32::from(self.is_encoded),
|
|
58
|
+
];
|
|
59
|
+
let length = header.len() + parts.iter().map(|part| part.len()).sum::<usize>();
|
|
60
|
+
let mut buffer = env.create_arraybuffer(length * 4)?;
|
|
61
|
+
// SAFETY: every bit pattern is a valid `u32`; `align_to_mut` puts any
|
|
62
|
+
// misaligned bytes in the prefix, which is checked to be empty.
|
|
63
|
+
let (prefix, words, _) = unsafe { buffer.align_to_mut::<u32>() };
|
|
64
|
+
if !prefix.is_empty() || words.len() < length {
|
|
65
|
+
return Err(napi::Error::from_reason("Unaligned snapshot buffer"));
|
|
66
|
+
}
|
|
67
|
+
let mut offset = 0;
|
|
68
|
+
for part in std::iter::once(&header[..]).chain(parts.iter().map(|part| part.as_slice())) {
|
|
69
|
+
words[offset..offset + part.len()].copy_from_slice(part);
|
|
70
|
+
offset += part.len();
|
|
71
|
+
}
|
|
72
|
+
let data: JsTypedArray =
|
|
73
|
+
buffer
|
|
74
|
+
.into_raw()
|
|
75
|
+
.into_typedarray(TypedArrayType::Uint32, length, 0)?;
|
|
76
|
+
let mut object = env.create_object()?;
|
|
77
|
+
object.set_named_property("data", data)?;
|
|
78
|
+
// V8 decodes UTF-8 slowly; text copies as Latin-1 when every
|
|
79
|
+
// character fits, which also keeps V8's compact one-byte string.
|
|
80
|
+
let text = if self.text.is_ascii() {
|
|
81
|
+
env.create_string_latin1(self.text.as_bytes())?
|
|
82
|
+
} else if let Some(latin1) = latin1_bytes(&self.text) {
|
|
83
|
+
env.create_string_latin1(&latin1)?
|
|
84
|
+
} else {
|
|
85
|
+
env.create_string_utf16(&utf16_units(&self.text))?
|
|
86
|
+
};
|
|
87
|
+
object.set_named_property("text", text)?;
|
|
88
|
+
Ok(object)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// `text` in Latin-1, if every character is below U+0100: UTF-8 encodes
|
|
93
|
+
/// those with lead bytes below 0xC4.
|
|
94
|
+
fn latin1_bytes(text: &str) -> Option<Vec<u8>> {
|
|
95
|
+
if text.bytes().any(|byte| byte >= 0xC4) {
|
|
96
|
+
return None;
|
|
97
|
+
}
|
|
98
|
+
Some(text.chars().map(|character| character as u8).collect())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// `text` as UTF-16, widening eight ASCII bytes at a time and decoding other
|
|
102
|
+
/// characters directly from their UTF-8 bytes, which `text` guarantees valid.
|
|
103
|
+
fn utf16_units(text: &str) -> Vec<u16> {
|
|
104
|
+
const HIGH_BITS: u64 = 0x8080_8080_8080_8080;
|
|
105
|
+
let bytes = text.as_bytes();
|
|
106
|
+
let mut units = Vec::with_capacity(bytes.len());
|
|
107
|
+
let mut index = 0;
|
|
108
|
+
while index < bytes.len() {
|
|
109
|
+
if let Some(chunk) = bytes.get(index..index + 8) {
|
|
110
|
+
let word = u64::from_le_bytes(chunk.try_into().unwrap());
|
|
111
|
+
if word & HIGH_BITS == 0 {
|
|
112
|
+
units.extend(chunk.iter().map(|byte| u16::from(*byte)));
|
|
113
|
+
index += 8;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let lead = u32::from(bytes[index]);
|
|
118
|
+
let continuation = |offset: usize| u32::from(bytes[index + offset] & 0x3F);
|
|
119
|
+
let (scalar, length) = match lead {
|
|
120
|
+
0x00..=0x7F => (lead, 1),
|
|
121
|
+
0x80..=0xDF => (((lead & 0x1F) << 6) | continuation(1), 2),
|
|
122
|
+
0xE0..=0xEF => (
|
|
123
|
+
((lead & 0x0F) << 12) | (continuation(1) << 6) | continuation(2),
|
|
124
|
+
3,
|
|
125
|
+
),
|
|
126
|
+
_ => (
|
|
127
|
+
((lead & 0x07) << 18)
|
|
128
|
+
| (continuation(1) << 12)
|
|
129
|
+
| (continuation(2) << 6)
|
|
130
|
+
| continuation(3),
|
|
131
|
+
4,
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
if scalar < 0x1_0000 {
|
|
135
|
+
units.push(scalar as u16);
|
|
136
|
+
} else {
|
|
137
|
+
let offset = scalar - 0x1_0000;
|
|
138
|
+
units.push(0xD800 | (offset >> 10) as u16);
|
|
139
|
+
units.push(0xDC00 | (offset & 0x3FF) as u16);
|
|
140
|
+
}
|
|
141
|
+
index += length;
|
|
142
|
+
}
|
|
143
|
+
units
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The UTF-16 length of `text`: one unit per character, two for characters
|
|
147
|
+
/// beyond the basic plane, counted from UTF-8 lead bytes.
|
|
148
|
+
fn utf16_length(text: &str) -> usize {
|
|
149
|
+
text.bytes()
|
|
150
|
+
.map(|byte| usize::from(byte & 0xC0 != 0x80) + usize::from(byte >= 0xF0))
|
|
151
|
+
.sum()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
struct Builder {
|
|
155
|
+
nodes: Vec<u32>,
|
|
156
|
+
children: Vec<u32>,
|
|
157
|
+
attributes: Vec<u32>,
|
|
158
|
+
strings: Vec<u32>,
|
|
159
|
+
text: String,
|
|
160
|
+
units: u32,
|
|
161
|
+
interned: IdMap<String, u32>,
|
|
162
|
+
/// html5ever names are atoms that hash and compare by identity.
|
|
163
|
+
local_names: IdMap<LocalName, u32>,
|
|
164
|
+
namespaces: IdMap<Namespace, u32>,
|
|
165
|
+
codec: bool,
|
|
166
|
+
is_encoded: bool,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub(crate) fn build(document: &NativeDocument) -> NodeSnapshot {
|
|
170
|
+
let codec = document.options.get("__utf16Codec") == Some(&Value::Bool(true))
|
|
171
|
+
|| document.options.get("__losslessUnicode") == Some(&Value::Bool(true));
|
|
172
|
+
let count = document.slot_count() as usize;
|
|
173
|
+
let mut builder = Builder {
|
|
174
|
+
nodes: Vec::with_capacity(count * STRIDE),
|
|
175
|
+
children: Vec::with_capacity(count),
|
|
176
|
+
attributes: Vec::new(),
|
|
177
|
+
strings: Vec::with_capacity(count * 2),
|
|
178
|
+
text: String::with_capacity(document.content_bytes.get()),
|
|
179
|
+
units: 0,
|
|
180
|
+
interned: IdMap::with_capacity_and_hasher(32, Default::default()),
|
|
181
|
+
local_names: IdMap::with_capacity_and_hasher(32, Default::default()),
|
|
182
|
+
namespaces: IdMap::default(),
|
|
183
|
+
codec,
|
|
184
|
+
is_encoded: false,
|
|
185
|
+
};
|
|
186
|
+
if let Some(arena) = document.arena.borrow().as_ref() {
|
|
187
|
+
builder.nodes.reserve(arena.nodes.len() * STRIDE);
|
|
188
|
+
for id in 0..arena.nodes.len() {
|
|
189
|
+
match arena.node(id as u32) {
|
|
190
|
+
Some(node) => builder.encode(node),
|
|
191
|
+
None => builder.released(),
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
builder.encode_immutable(document);
|
|
196
|
+
}
|
|
197
|
+
NodeSnapshot {
|
|
198
|
+
is_encoded: builder.is_encoded,
|
|
199
|
+
nodes: builder.nodes,
|
|
200
|
+
children: builder.children,
|
|
201
|
+
attributes: builder.attributes,
|
|
202
|
+
strings: builder.strings,
|
|
203
|
+
text: builder.text,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// The JSON fields a document root's record carries, as `convert_node` sets them.
|
|
208
|
+
fn quirks_mode_json(mode: html5ever::tree_builder::QuirksMode) -> &'static str {
|
|
209
|
+
match mode {
|
|
210
|
+
html5ever::tree_builder::QuirksMode::Quirks => r#"{"x-mode":"quirks"}"#,
|
|
211
|
+
html5ever::tree_builder::QuirksMode::LimitedQuirks => r#"{"x-mode":"limited-quirks"}"#,
|
|
212
|
+
html5ever::tree_builder::QuirksMode::NoQuirks => r#"{"x-mode":"no-quirks"}"#,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// The JSON fields a doctype's record carries, as `convert_node` sets them.
|
|
217
|
+
fn doctype_fields_json(doctype: &scraper::node::Doctype) -> String {
|
|
218
|
+
let quoted = |value: &str| serde_json::to_string(value).unwrap_or_else(|_| "\"\"".into());
|
|
219
|
+
format!(
|
|
220
|
+
r#"{{"x-name":{},"x-publicId":{},"x-systemId":{}}}"#,
|
|
221
|
+
quoted(doctype.name()),
|
|
222
|
+
quoted(doctype.public_id()),
|
|
223
|
+
quoted(doctype.system_id())
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn pointer(value: Option<u32>) -> u32 {
|
|
228
|
+
value.map_or(0, |id| id + 1)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
impl Builder {
|
|
232
|
+
fn string(&mut self, value: &str) -> u32 {
|
|
233
|
+
let start = self.units;
|
|
234
|
+
self.text.push_str(value);
|
|
235
|
+
let units = if value.is_ascii() {
|
|
236
|
+
value.len()
|
|
237
|
+
} else {
|
|
238
|
+
self.is_encoded |= self.codec && crate::codec::has_encoding(value);
|
|
239
|
+
utf16_length(value)
|
|
240
|
+
};
|
|
241
|
+
self.units += units as u32;
|
|
242
|
+
self.strings.push(start);
|
|
243
|
+
self.strings.push(self.units);
|
|
244
|
+
(self.strings.len() / 2 - 1) as u32
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn interned(&mut self, value: &str) -> u32 {
|
|
248
|
+
if let Some(reference) = self.interned.get(value) {
|
|
249
|
+
return *reference;
|
|
250
|
+
}
|
|
251
|
+
let reference = self.string(value);
|
|
252
|
+
self.interned.insert(value.to_owned(), reference);
|
|
253
|
+
reference
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fn local_name(&mut self, name: &LocalName) -> u32 {
|
|
257
|
+
if let Some(reference) = self.local_names.get(name) {
|
|
258
|
+
return *reference;
|
|
259
|
+
}
|
|
260
|
+
let reference = self.interned(name);
|
|
261
|
+
self.local_names.insert(name.clone(), reference);
|
|
262
|
+
reference
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn namespace(&mut self, namespace: &Namespace) -> u32 {
|
|
266
|
+
if let Some(reference) = self.namespaces.get(namespace) {
|
|
267
|
+
return *reference;
|
|
268
|
+
}
|
|
269
|
+
let reference = self.interned(namespace);
|
|
270
|
+
self.namespaces.insert(namespace.clone(), reference);
|
|
271
|
+
reference
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn released(&mut self) {
|
|
275
|
+
let mut fields = [0; STRIDE];
|
|
276
|
+
fields[10] = RELEASED;
|
|
277
|
+
self.nodes.extend_from_slice(&fields);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn encode(&mut self, node: &DomNode) {
|
|
281
|
+
let mut fields = [0; STRIDE];
|
|
282
|
+
fields[0] = self.interned(&node.kind);
|
|
283
|
+
fields[1] = pointer(node.parent);
|
|
284
|
+
fields[2] = pointer(node.prev);
|
|
285
|
+
fields[3] = pointer(node.next);
|
|
286
|
+
fields[4] = self.children.len() as u32;
|
|
287
|
+
fields[5] = node.children.len() as u32;
|
|
288
|
+
self.children.extend_from_slice(&node.children);
|
|
289
|
+
fields[6] = node
|
|
290
|
+
.name
|
|
291
|
+
.as_deref()
|
|
292
|
+
.map_or(0, |name| self.interned(name) + 1);
|
|
293
|
+
let mut flags = 0;
|
|
294
|
+
match node.data.as_deref() {
|
|
295
|
+
Some(data) if data.len() > LARGE_TEXT_BYTES => flags |= OPAQUE,
|
|
296
|
+
Some(data) => fields[7] = self.string(data) + 1,
|
|
297
|
+
None => {}
|
|
298
|
+
}
|
|
299
|
+
fields[8] = (self.attributes.len() / 2) as u32;
|
|
300
|
+
fields[9] = node.attribs.len() as u32;
|
|
301
|
+
for (name, value) in &node.attribs {
|
|
302
|
+
let Some(value) = value.as_str() else {
|
|
303
|
+
flags |= OPAQUE;
|
|
304
|
+
break;
|
|
305
|
+
};
|
|
306
|
+
let name = self.interned(name);
|
|
307
|
+
let value = self.string(value);
|
|
308
|
+
self.attributes.extend_from_slice(&[name, value]);
|
|
309
|
+
}
|
|
310
|
+
fields[11] = ABSENT;
|
|
311
|
+
fields[12] = ABSENT;
|
|
312
|
+
let mut rest = Map::new();
|
|
313
|
+
if let Some(namespace) = &node.namespace {
|
|
314
|
+
flags |= HAS_NAMESPACE;
|
|
315
|
+
fields[13] = self.interned(namespace);
|
|
316
|
+
}
|
|
317
|
+
match (&node.attribs_namespace, &node.attribs_prefix) {
|
|
318
|
+
(Some(namespaces), Some(prefixes)) if namespaces.is_empty() && prefixes.is_empty() => {
|
|
319
|
+
flags |= EMPTY_ATTRIBUTE_MAPS;
|
|
320
|
+
}
|
|
321
|
+
(namespaces, prefixes) => {
|
|
322
|
+
if let Some(namespaces) = namespaces {
|
|
323
|
+
rest.insert(
|
|
324
|
+
"attribsNamespace".into(),
|
|
325
|
+
Value::Object((**namespaces).clone()),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if let Some(prefixes) = prefixes {
|
|
329
|
+
rest.insert("attribsPrefix".into(), Value::Object((**prefixes).clone()));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (key, value) in &node.extras {
|
|
334
|
+
let index = value.as_u64().filter(|index| *index < u64::from(ABSENT));
|
|
335
|
+
match (key.as_str(), value) {
|
|
336
|
+
("startIndex", Value::Null) => flags |= HAS_START_INDEX,
|
|
337
|
+
("startIndex", _) if index.is_some() => {
|
|
338
|
+
flags |= HAS_START_INDEX;
|
|
339
|
+
fields[11] = index.unwrap() as u32;
|
|
340
|
+
}
|
|
341
|
+
("endIndex", Value::Null) => flags |= HAS_END_INDEX,
|
|
342
|
+
("endIndex", _) if index.is_some() => {
|
|
343
|
+
flags |= HAS_END_INDEX;
|
|
344
|
+
fields[12] = index.unwrap() as u32;
|
|
345
|
+
}
|
|
346
|
+
_ => {
|
|
347
|
+
rest.insert(key.clone(), value.clone());
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if !rest.is_empty() {
|
|
352
|
+
match serde_json::to_string(&rest) {
|
|
353
|
+
Ok(json) => fields[14] = self.interned(&json) + 1,
|
|
354
|
+
Err(_) => flags |= OPAQUE,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
fields[10] = flags;
|
|
358
|
+
self.nodes.extend_from_slice(&fields);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Mirrors `NativeDocument::immutable_dom_node` without building records
|
|
362
|
+
/// for elements, text and comments; other nodes use that record directly.
|
|
363
|
+
fn encode_immutable(&mut self, document: &NativeDocument) {
|
|
364
|
+
let html = document.html.borrow();
|
|
365
|
+
let ids = document.ids.borrow();
|
|
366
|
+
let reverse = document.reverse.borrow();
|
|
367
|
+
let Some(html) = html.as_ref() else {
|
|
368
|
+
return;
|
|
369
|
+
};
|
|
370
|
+
let local = |node: Option<ego_tree::NodeRef<'_, Node>>| {
|
|
371
|
+
node.and_then(|node| reverse.get(&node.id()).copied())
|
|
372
|
+
};
|
|
373
|
+
let text_type = self.interned("text");
|
|
374
|
+
let comment_type = self.interned("comment");
|
|
375
|
+
let tag_type = self.interned("tag");
|
|
376
|
+
let script_type = self.interned("script");
|
|
377
|
+
let style_type = self.interned("style");
|
|
378
|
+
for (index, node_id) in ids.iter().enumerate() {
|
|
379
|
+
let Some(node) = html.tree.get(*node_id) else {
|
|
380
|
+
self.released();
|
|
381
|
+
continue;
|
|
382
|
+
};
|
|
383
|
+
let is_plain_element = matches!(node.value(), Node::Element(element)
|
|
384
|
+
if element.name.prefix.is_none()
|
|
385
|
+
&& element.attrs.iter().all(|(name, _)| name.ns.is_empty() && name.prefix.is_none()));
|
|
386
|
+
let is_direct = is_plain_element
|
|
387
|
+
|| matches!(
|
|
388
|
+
node.value(),
|
|
389
|
+
Node::Text(_)
|
|
390
|
+
| Node::Comment(_)
|
|
391
|
+
| Node::Document
|
|
392
|
+
| Node::Fragment
|
|
393
|
+
| Node::Doctype(_)
|
|
394
|
+
);
|
|
395
|
+
if !is_direct {
|
|
396
|
+
match document.immutable_dom_node(index as u32) {
|
|
397
|
+
Some(record) => self.encode(&record),
|
|
398
|
+
None => self.released(),
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
let mut fields = [0; STRIDE];
|
|
403
|
+
fields[1] = pointer(local(node.parent()));
|
|
404
|
+
fields[2] = pointer(local(node.prev_sibling()));
|
|
405
|
+
fields[3] = pointer(local(node.next_sibling()));
|
|
406
|
+
fields[4] = self.children.len() as u32;
|
|
407
|
+
let first_child = self.children.len();
|
|
408
|
+
self.children.extend(
|
|
409
|
+
node.children()
|
|
410
|
+
.filter_map(|child| reverse.get(&child.id()).copied()),
|
|
411
|
+
);
|
|
412
|
+
fields[5] = (self.children.len() - first_child) as u32;
|
|
413
|
+
fields[8] = (self.attributes.len() / 2) as u32;
|
|
414
|
+
fields[11] = ABSENT;
|
|
415
|
+
fields[12] = ABSENT;
|
|
416
|
+
let mut flags = HAS_START_INDEX | HAS_END_INDEX;
|
|
417
|
+
match node.value() {
|
|
418
|
+
Node::Document => {
|
|
419
|
+
fields[0] = self.interned("root");
|
|
420
|
+
fields[14] = self.interned(quirks_mode_json(html.quirks_mode)) + 1;
|
|
421
|
+
flags = 0;
|
|
422
|
+
}
|
|
423
|
+
Node::Fragment => {
|
|
424
|
+
fields[0] = self.interned("root");
|
|
425
|
+
flags = 0;
|
|
426
|
+
}
|
|
427
|
+
Node::Doctype(doctype) => {
|
|
428
|
+
fields[0] = self.interned("directive");
|
|
429
|
+
fields[6] = self.interned("!doctype") + 1;
|
|
430
|
+
fields[7] = self.string(&crate::doctype_content(doctype)) + 1;
|
|
431
|
+
fields[14] = self.interned(&doctype_fields_json(doctype)) + 1;
|
|
432
|
+
flags = 0;
|
|
433
|
+
}
|
|
434
|
+
Node::Text(text) => {
|
|
435
|
+
fields[0] = text_type;
|
|
436
|
+
if text.len() > LARGE_TEXT_BYTES {
|
|
437
|
+
flags |= OPAQUE;
|
|
438
|
+
} else {
|
|
439
|
+
fields[7] = self.string(text) + 1;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
Node::Comment(comment) => {
|
|
443
|
+
fields[0] = comment_type;
|
|
444
|
+
if comment.len() > LARGE_TEXT_BYTES {
|
|
445
|
+
flags |= OPAQUE;
|
|
446
|
+
} else {
|
|
447
|
+
fields[7] = self.string(comment) + 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
Node::Element(element) => {
|
|
451
|
+
fields[0] = match element.name() {
|
|
452
|
+
"script" => script_type,
|
|
453
|
+
"style" => style_type,
|
|
454
|
+
_ => tag_type,
|
|
455
|
+
};
|
|
456
|
+
fields[6] = self.local_name(&element.name.local) + 1;
|
|
457
|
+
for (attribute, value) in &element.attrs {
|
|
458
|
+
let attribute = self.local_name(&attribute.local);
|
|
459
|
+
let value = self.string(value);
|
|
460
|
+
self.attributes.extend_from_slice(&[attribute, value]);
|
|
461
|
+
}
|
|
462
|
+
fields[9] = element.attrs.len() as u32;
|
|
463
|
+
fields[13] = self.namespace(&element.name.ns);
|
|
464
|
+
flags |= HAS_NAMESPACE | EMPTY_ATTRIBUTE_MAPS;
|
|
465
|
+
}
|
|
466
|
+
_ => unreachable!("other node types use the record path"),
|
|
467
|
+
}
|
|
468
|
+
fields[10] = flags;
|
|
469
|
+
self.nodes.extend_from_slice(&fields);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#[cfg(test)]
|
|
475
|
+
mod tests {
|
|
476
|
+
use super::*;
|
|
477
|
+
|
|
478
|
+
#[test]
|
|
479
|
+
fn transcodes_text_like_the_standard_library() {
|
|
480
|
+
for text in [
|
|
481
|
+
"",
|
|
482
|
+
"ascii only",
|
|
483
|
+
"café ©",
|
|
484
|
+
"번개장터 “quoted” — dash",
|
|
485
|
+
"emoji 😀 and \u{e800}",
|
|
486
|
+
"\u{ff}\u{100}",
|
|
487
|
+
"a\u{10ffff}b",
|
|
488
|
+
] {
|
|
489
|
+
let expected: Vec<u16> = text.encode_utf16().collect();
|
|
490
|
+
assert_eq!(utf16_units(text), expected, "{text}");
|
|
491
|
+
assert_eq!(utf16_length(text), expected.len(), "{text}");
|
|
492
|
+
let latin1 = latin1_bytes(text);
|
|
493
|
+
assert_eq!(
|
|
494
|
+
latin1.is_some(),
|
|
495
|
+
text.chars().all(|character| (character as u32) < 0x100),
|
|
496
|
+
"{text}"
|
|
497
|
+
);
|
|
498
|
+
if let Some(bytes) = latin1 {
|
|
499
|
+
assert_eq!(
|
|
500
|
+
bytes
|
|
501
|
+
.iter()
|
|
502
|
+
.map(|byte| u16::from(*byte))
|
|
503
|
+
.collect::<Vec<_>>(),
|
|
504
|
+
expected
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
package/native/src/stream.rs
CHANGED
|
@@ -211,7 +211,7 @@ impl Task for FinishTask {
|
|
|
211
211
|
}
|
|
212
212
|
enum Parser {
|
|
213
213
|
Html(Box<html5ever::driver::Parser<sink::CompatibleSink>>),
|
|
214
|
-
Xml(arena::XmlStream),
|
|
214
|
+
Xml(Box<arena::XmlStream>),
|
|
215
215
|
}
|
|
216
216
|
fn run(
|
|
217
217
|
receiver: mpsc::Receiver<Message>,
|
|
@@ -222,7 +222,7 @@ fn run(
|
|
|
222
222
|
let parser = if arena::xml_mode(&options)
|
|
223
223
|
|| options.get("_useHtmlParser2") == Some(&Value::Bool(true))
|
|
224
224
|
{
|
|
225
|
-
Parser::Xml(arena::XmlStream::new(options.clone()))
|
|
225
|
+
Parser::Xml(Box::new(arena::XmlStream::new(options.clone())))
|
|
226
226
|
} else {
|
|
227
227
|
let parse_options = html5ever::driver::ParseOpts {
|
|
228
228
|
tokenizer: html5ever::tokenizer::TokenizerOpts {
|
package/native/src/tokenizer.rs
CHANGED
|
@@ -23,6 +23,8 @@ struct Builder<'a> {
|
|
|
23
23
|
xml: bool,
|
|
24
24
|
lower_tags: bool,
|
|
25
25
|
lower_attrs: bool,
|
|
26
|
+
with_end_indices: bool,
|
|
27
|
+
recognize_self_closing: Option<bool>,
|
|
26
28
|
}
|
|
27
29
|
impl<'a> Builder<'a> {
|
|
28
30
|
fn new(source: &'a Source<'a>, options: Value) -> Self {
|
|
@@ -35,8 +37,13 @@ impl<'a> Builder<'a> {
|
|
|
35
37
|
.get("lowerCaseAttributeNames")
|
|
36
38
|
.and_then(Value::as_bool)
|
|
37
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);
|
|
38
45
|
Self {
|
|
39
|
-
arena
|
|
46
|
+
arena,
|
|
40
47
|
source,
|
|
41
48
|
parents: vec![0],
|
|
42
49
|
foreign: vec![xml],
|
|
@@ -48,6 +55,8 @@ impl<'a> Builder<'a> {
|
|
|
48
55
|
xml,
|
|
49
56
|
lower_tags,
|
|
50
57
|
lower_attrs,
|
|
58
|
+
with_end_indices,
|
|
59
|
+
recognize_self_closing,
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
fn slice(&self, start: usize, end: usize) -> String {
|
|
@@ -63,7 +72,7 @@ impl<'a> Builder<'a> {
|
|
|
63
72
|
}
|
|
64
73
|
fn close(&mut self) {
|
|
65
74
|
if let Some(id) = (self.parents.len() > 1).then(|| self.parents.pop().unwrap()) {
|
|
66
|
-
if self.
|
|
75
|
+
if self.with_end_indices {
|
|
67
76
|
self.arena.nodes[id as usize]
|
|
68
77
|
.extras
|
|
69
78
|
.insert("endIndex".into(), json!(self.end));
|
|
@@ -109,12 +118,14 @@ impl<'a> Builder<'a> {
|
|
|
109
118
|
fn end_open(&mut self, self_closing: bool) {
|
|
110
119
|
if let Some(pending) = self.pending.take() {
|
|
111
120
|
let is_void = !self.xml && html_parser_void(&pending.name);
|
|
112
|
-
let kind =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"tag"
|
|
121
|
+
let kind = match pending.name.as_str() {
|
|
122
|
+
"script" if !self.xml => "script",
|
|
123
|
+
"style" if !self.xml => "style",
|
|
124
|
+
_ => "tag",
|
|
116
125
|
};
|
|
117
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());
|
|
118
129
|
node.name = Some(pending.name);
|
|
119
130
|
node.attribs = pending.attrs;
|
|
120
131
|
let id = append_parsed(
|
|
@@ -124,6 +135,17 @@ impl<'a> Builder<'a> {
|
|
|
124
135
|
pending.start,
|
|
125
136
|
self.end,
|
|
126
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
|
+
}
|
|
127
149
|
if !is_void && !self_closing {
|
|
128
150
|
self.parents.push(id);
|
|
129
151
|
}
|
|
@@ -370,12 +392,7 @@ impl Builder<'_> {
|
|
|
370
392
|
}
|
|
371
393
|
fn on_self_closing_tag(&mut self, end: usize) {
|
|
372
394
|
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)
|
|
395
|
+
let self_closing = self.recognize_self_closing.unwrap_or(self.xml)
|
|
379
396
|
|| *self.foreign.last().unwrap_or(&self.xml);
|
|
380
397
|
self.end_open(self_closing);
|
|
381
398
|
self.start = end + 1;
|