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/arena.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//! Mutable, stable-ID DOM storage. All mutations stay on the native side.
|
|
2
|
+
use std::borrow::Cow;
|
|
2
3
|
use std::collections::{HashMap, HashSet};
|
|
3
4
|
|
|
4
5
|
use napi::{Error, Result, Status};
|
|
@@ -10,7 +11,7 @@ pub(crate) struct DomNode {
|
|
|
10
11
|
#[serde(default)]
|
|
11
12
|
pub id: u32,
|
|
12
13
|
#[serde(rename = "type", default = "root_kind")]
|
|
13
|
-
pub kind:
|
|
14
|
+
pub kind: Cow<'static, str>,
|
|
14
15
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
15
16
|
pub name: Option<String>,
|
|
16
17
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
@@ -25,16 +26,65 @@ pub(crate) struct DomNode {
|
|
|
25
26
|
pub prev: Option<u32>,
|
|
26
27
|
#[serde(default)]
|
|
27
28
|
pub next: Option<u32>,
|
|
29
|
+
/// Element namespace; borrowed for the namespaces HTML parsing produces.
|
|
30
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
31
|
+
pub namespace: Option<Cow<'static, str>>,
|
|
32
|
+
/// parse5's per-attribute namespace and prefix maps, when the node has them.
|
|
33
|
+
#[serde(
|
|
34
|
+
rename = "attribsNamespace",
|
|
35
|
+
default,
|
|
36
|
+
skip_serializing_if = "Option::is_none"
|
|
37
|
+
)]
|
|
38
|
+
pub attribs_namespace: Option<Box<Map<String, Value>>>,
|
|
39
|
+
#[serde(
|
|
40
|
+
rename = "attribsPrefix",
|
|
41
|
+
default,
|
|
42
|
+
skip_serializing_if = "Option::is_none"
|
|
43
|
+
)]
|
|
44
|
+
pub attribs_prefix: Option<Box<Map<String, Value>>>,
|
|
28
45
|
#[serde(flatten)]
|
|
29
46
|
pub extras: Map<String, Value>,
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
fn root_kind() ->
|
|
33
|
-
"root"
|
|
49
|
+
fn root_kind() -> Cow<'static, str> {
|
|
50
|
+
Cow::Borrowed("root")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(crate) const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
|
|
54
|
+
|
|
55
|
+
/// A namespace, borrowed for the ones HTML parsing produces.
|
|
56
|
+
pub(crate) fn namespace_name(namespace: &str) -> Cow<'static, str> {
|
|
57
|
+
match namespace {
|
|
58
|
+
HTML_NAMESPACE => Cow::Borrowed(HTML_NAMESPACE),
|
|
59
|
+
"http://www.w3.org/2000/svg" => Cow::Borrowed("http://www.w3.org/2000/svg"),
|
|
60
|
+
"http://www.w3.org/1998/Math/MathML" => Cow::Borrowed("http://www.w3.org/1998/Math/MathML"),
|
|
61
|
+
"http://www.w3.org/1999/xlink" => Cow::Borrowed("http://www.w3.org/1999/xlink"),
|
|
62
|
+
"http://www.w3.org/XML/1998/namespace" => {
|
|
63
|
+
Cow::Borrowed("http://www.w3.org/XML/1998/namespace")
|
|
64
|
+
}
|
|
65
|
+
"http://www.w3.org/2000/xmlns/" => Cow::Borrowed("http://www.w3.org/2000/xmlns/"),
|
|
66
|
+
other => Cow::Owned(other.to_owned()),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// A node type, borrowed for the types the DOM defines.
|
|
71
|
+
pub(crate) fn node_kind(kind: &str) -> Cow<'static, str> {
|
|
72
|
+
match kind {
|
|
73
|
+
"tag" => Cow::Borrowed("tag"),
|
|
74
|
+
"text" => Cow::Borrowed("text"),
|
|
75
|
+
"root" => Cow::Borrowed("root"),
|
|
76
|
+
"script" => Cow::Borrowed("script"),
|
|
77
|
+
"style" => Cow::Borrowed("style"),
|
|
78
|
+
"comment" => Cow::Borrowed("comment"),
|
|
79
|
+
"directive" => Cow::Borrowed("directive"),
|
|
80
|
+
"cdata" => Cow::Borrowed("cdata"),
|
|
81
|
+
"doctype" => Cow::Borrowed("doctype"),
|
|
82
|
+
other => Cow::Owned(other.to_owned()),
|
|
83
|
+
}
|
|
34
84
|
}
|
|
35
85
|
|
|
36
86
|
impl DomNode {
|
|
37
|
-
pub(crate) fn new(kind:
|
|
87
|
+
pub(crate) fn new(kind: impl Into<Cow<'static, str>>) -> Self {
|
|
38
88
|
Self {
|
|
39
89
|
id: 0,
|
|
40
90
|
kind: kind.into(),
|
|
@@ -45,11 +95,14 @@ impl DomNode {
|
|
|
45
95
|
parent: None,
|
|
46
96
|
prev: None,
|
|
47
97
|
next: None,
|
|
98
|
+
namespace: None,
|
|
99
|
+
attribs_namespace: None,
|
|
100
|
+
attribs_prefix: None,
|
|
48
101
|
extras: Map::new(),
|
|
49
102
|
}
|
|
50
103
|
}
|
|
51
104
|
pub(crate) fn is_element(&self) -> bool {
|
|
52
|
-
matches!(self.kind.
|
|
105
|
+
matches!(self.kind.as_ref(), "tag" | "script" | "style")
|
|
53
106
|
}
|
|
54
107
|
pub(crate) fn content_bytes(&self) -> usize {
|
|
55
108
|
let attributes: usize = self
|
|
@@ -67,25 +120,106 @@ pub(crate) struct DomArena {
|
|
|
67
120
|
pub nodes: Vec<DomNode>,
|
|
68
121
|
pub options: Value,
|
|
69
122
|
pub free: Vec<u32>,
|
|
123
|
+
/// Parents whose child lists changed since JavaScript last asked, so it
|
|
124
|
+
/// can refresh the child arrays it holds without rereading every node.
|
|
125
|
+
pub touched: Vec<u32>,
|
|
126
|
+
/// Too many changes were recorded to list; every parent counts as touched.
|
|
127
|
+
pub touched_overflow: bool,
|
|
128
|
+
/// Whether parsing records start and end indices, read once per parse.
|
|
129
|
+
pub parse_indices: Option<(bool, bool)>,
|
|
130
|
+
/// Nodes removed from a parent since the last collection.
|
|
131
|
+
pub detached_since_collection: u32,
|
|
132
|
+
/// Live nodes after the last collection, or when the arena was built.
|
|
133
|
+
pub collected_live: u32,
|
|
134
|
+
/// Elements by ASCII-lowercased name, in document order, as parsing
|
|
135
|
+
/// created them; dropped before any write, which may reorder ids.
|
|
136
|
+
pub name_index: Option<NameIndex>,
|
|
70
137
|
}
|
|
71
138
|
|
|
139
|
+
pub(crate) type NameIndex = crate::id_hash::IdMap<String, Vec<u32>>;
|
|
140
|
+
|
|
141
|
+
const TOUCHED_LIMIT: usize = 1 << 16;
|
|
142
|
+
|
|
72
143
|
impl DomArena {
|
|
144
|
+
/// An arena over records whose ids and links are already consistent, as
|
|
145
|
+
/// a parse tree's conversion produces them.
|
|
146
|
+
pub(crate) fn from_linked_records(nodes: Vec<DomNode>, options: Value) -> Self {
|
|
147
|
+
let slots = nodes.len() as u32;
|
|
148
|
+
Self {
|
|
149
|
+
nodes,
|
|
150
|
+
options,
|
|
151
|
+
free: Vec::new(),
|
|
152
|
+
touched: Vec::new(),
|
|
153
|
+
touched_overflow: false,
|
|
154
|
+
parse_indices: None,
|
|
155
|
+
detached_since_collection: 0,
|
|
156
|
+
collected_live: slots,
|
|
157
|
+
name_index: None,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
73
160
|
pub(crate) fn from_records(mut nodes: Vec<DomNode>, options: Value) -> Self {
|
|
74
161
|
for (id, node) in nodes.iter_mut().enumerate() {
|
|
75
162
|
node.id = id as u32;
|
|
76
|
-
node.extras.entry("startIndex").or_insert(Value::Null);
|
|
77
|
-
node.extras.entry("endIndex").or_insert(Value::Null);
|
|
78
163
|
}
|
|
164
|
+
let slots = nodes.len() as u32;
|
|
79
165
|
let mut arena = Self {
|
|
80
166
|
nodes,
|
|
81
167
|
options,
|
|
82
168
|
free: Vec::new(),
|
|
169
|
+
touched: Vec::new(),
|
|
170
|
+
touched_overflow: false,
|
|
171
|
+
parse_indices: None,
|
|
172
|
+
detached_since_collection: 0,
|
|
173
|
+
collected_live: slots,
|
|
174
|
+
name_index: None,
|
|
83
175
|
};
|
|
84
176
|
for id in 0..arena.nodes.len() {
|
|
85
177
|
arena.relink(id as u32);
|
|
86
178
|
}
|
|
179
|
+
arena.take_touched();
|
|
87
180
|
arena
|
|
88
181
|
}
|
|
182
|
+
pub(crate) fn touch(&mut self, parent: u32) {
|
|
183
|
+
if self.touched_overflow {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if self.touched.len() >= TOUCHED_LIMIT {
|
|
187
|
+
self.touched = Vec::new();
|
|
188
|
+
self.touched_overflow = true;
|
|
189
|
+
} else {
|
|
190
|
+
self.touched.push(parent);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/// The parents touched since the last call, or `None` when too many were.
|
|
194
|
+
pub(crate) fn take_touched(&mut self) -> Option<Vec<u32>> {
|
|
195
|
+
let touched = std::mem::take(&mut self.touched);
|
|
196
|
+
let overflow = std::mem::replace(&mut self.touched_overflow, false);
|
|
197
|
+
(!overflow).then_some(touched)
|
|
198
|
+
}
|
|
199
|
+
fn note_detached(&mut self) {
|
|
200
|
+
self.detached_since_collection = self.detached_since_collection.saturating_add(1);
|
|
201
|
+
}
|
|
202
|
+
/// Every live node that `root` does not reach through child links.
|
|
203
|
+
pub(crate) fn detached_slots(&self, root: u32) -> Vec<u32> {
|
|
204
|
+
let mut attached = vec![false; self.nodes.len()];
|
|
205
|
+
let mut stack = vec![root];
|
|
206
|
+
while let Some(id) = stack.pop() {
|
|
207
|
+
let Some(node) = self.node(id) else { continue };
|
|
208
|
+
if std::mem::replace(&mut attached[id as usize], true) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
stack.extend(node.children.iter().copied());
|
|
212
|
+
}
|
|
213
|
+
(0..self.nodes.len() as u32)
|
|
214
|
+
.filter(|id| !attached[*id as usize] && self.node(*id).is_some())
|
|
215
|
+
.collect()
|
|
216
|
+
}
|
|
217
|
+
/// Whether removed nodes may have left enough garbage to collect: the
|
|
218
|
+
/// live slots have grown by half since the last collection.
|
|
219
|
+
pub(crate) fn is_collection_due(&self) -> bool {
|
|
220
|
+
let live = self.nodes.len().saturating_sub(self.free.len()) as u64;
|
|
221
|
+
self.detached_since_collection > 0 && live >= u64::from(self.collected_live) * 3 / 2 + 256
|
|
222
|
+
}
|
|
89
223
|
pub(crate) fn node(&self, id: u32) -> Option<&DomNode> {
|
|
90
224
|
self.nodes
|
|
91
225
|
.get(id as usize)
|
|
@@ -124,7 +258,7 @@ impl DomArena {
|
|
|
124
258
|
.insert(name.into(), Value::String(value.into()));
|
|
125
259
|
}
|
|
126
260
|
None => {
|
|
127
|
-
node.attribs.
|
|
261
|
+
node.attribs.shift_remove(name);
|
|
128
262
|
}
|
|
129
263
|
}
|
|
130
264
|
}
|
|
@@ -132,6 +266,7 @@ impl DomArena {
|
|
|
132
266
|
}
|
|
133
267
|
}
|
|
134
268
|
pub(crate) fn relink(&mut self, parent: u32) {
|
|
269
|
+
self.touch(parent);
|
|
135
270
|
let children = self
|
|
136
271
|
.nodes
|
|
137
272
|
.get(parent as usize)
|
|
@@ -150,6 +285,7 @@ impl DomArena {
|
|
|
150
285
|
}
|
|
151
286
|
pub(crate) fn detach(&mut self, id: u32) {
|
|
152
287
|
if let Some(parent) = self.node(id).and_then(|node| node.parent) {
|
|
288
|
+
self.note_detached();
|
|
153
289
|
if let Some(node) = self.nodes.get_mut(parent as usize) {
|
|
154
290
|
node.children.retain(|child| *child != id);
|
|
155
291
|
}
|
|
@@ -169,6 +305,11 @@ impl DomArena {
|
|
|
169
305
|
return;
|
|
170
306
|
}
|
|
171
307
|
let removed: HashSet<_> = ids.iter().copied().collect();
|
|
308
|
+
for id in ids {
|
|
309
|
+
if self.node(*id).is_some_and(|node| node.parent.is_some()) {
|
|
310
|
+
self.note_detached();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
172
313
|
let parents: HashSet<_> = ids
|
|
173
314
|
.iter()
|
|
174
315
|
.filter_map(|id| self.node(*id).and_then(|node| node.parent))
|
|
@@ -270,6 +411,7 @@ impl DomArena {
|
|
|
270
411
|
index += 1;
|
|
271
412
|
}
|
|
272
413
|
self.nodes[id as usize].children = children;
|
|
414
|
+
self.touch(id);
|
|
273
415
|
}
|
|
274
416
|
}
|
|
275
417
|
self.inner_html(id)
|
|
@@ -280,8 +422,12 @@ impl DomArena {
|
|
|
280
422
|
pub(crate) fn serialize(&self, ids: &[u32], options: &Value) -> String {
|
|
281
423
|
let mut output = String::new();
|
|
282
424
|
let xml = xml_mode(options);
|
|
283
|
-
|
|
284
|
-
|
|
425
|
+
// Cheerio serializes HTML with parse5, which always escapes; only the
|
|
426
|
+
// htmlparser2 serializer leaves entities alone when asked to.
|
|
427
|
+
let is_parse5 = !xml && options.get("_useHtmlParser2") != Some(&Value::Bool(true));
|
|
428
|
+
let encode = is_parse5
|
|
429
|
+
|| (options.get("decodeEntities") != Some(&Value::Bool(false))
|
|
430
|
+
&& options.get("encodeEntities") != Some(&Value::Bool(false)));
|
|
285
431
|
let mut stack: Vec<(u32, bool)> = ids.iter().rev().map(|id| (*id, false)).collect();
|
|
286
432
|
let mut budget = self.nodes.len().saturating_mul(8).saturating_add(1024);
|
|
287
433
|
while let Some((id, close)) = stack.pop() {
|
|
@@ -298,7 +444,7 @@ impl DomArena {
|
|
|
298
444
|
output.push('>');
|
|
299
445
|
continue;
|
|
300
446
|
}
|
|
301
|
-
match node.kind.
|
|
447
|
+
match node.kind.as_ref() {
|
|
302
448
|
"root" => stack.extend(node.children.iter().rev().map(|id| (*id, false))),
|
|
303
449
|
"text" => {
|
|
304
450
|
let raw = !xml
|
|
@@ -366,13 +512,16 @@ impl DomArena {
|
|
|
366
512
|
// rendering write the stored attribute names unchanged.
|
|
367
513
|
let qualifiers = (!xml
|
|
368
514
|
&& options.get("_useHtmlParser2") != Some(&Value::Bool(true)))
|
|
369
|
-
.
|
|
370
|
-
(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
515
|
+
.then_some((
|
|
516
|
+
node.attribs_namespace.as_deref(),
|
|
517
|
+
node.attribs_prefix.as_deref(),
|
|
518
|
+
));
|
|
519
|
+
let attributes = super::js_property_order(
|
|
520
|
+
node.attribs
|
|
521
|
+
.iter()
|
|
522
|
+
.map(|(name, value)| (name.as_str(), value)),
|
|
523
|
+
);
|
|
524
|
+
for (name, value) in attributes {
|
|
376
525
|
output.push(' ');
|
|
377
526
|
match qualifiers {
|
|
378
527
|
Some((namespaces, prefixes)) => super::push_attribute_name(
|
|
@@ -415,9 +564,8 @@ impl DomArena {
|
|
|
415
564
|
}
|
|
416
565
|
let is_void = !xml
|
|
417
566
|
&& (node
|
|
418
|
-
.
|
|
419
|
-
.
|
|
420
|
-
.and_then(Value::as_str)
|
|
567
|
+
.namespace
|
|
568
|
+
.as_deref()
|
|
421
569
|
.is_none_or(|namespace| namespace == super::HTML_NS))
|
|
422
570
|
&& super::void_name(name);
|
|
423
571
|
if xml
|
|
@@ -529,28 +677,44 @@ impl DomArena {
|
|
|
529
677
|
roots
|
|
530
678
|
}
|
|
531
679
|
pub(crate) fn create_fragment(&mut self, html: &str, context: Option<u32>) -> Result<Vec<u32>> {
|
|
532
|
-
let
|
|
533
|
-
|
|
534
|
-
|
|
680
|
+
let parsed = self.parse_fragment(html, context)?;
|
|
681
|
+
let roots = parsed.nodes[0].children.clone();
|
|
682
|
+
Ok(self.import_internal(&parsed.nodes, &roots, false))
|
|
683
|
+
}
|
|
684
|
+
/// Parses `html` as the children of `context`; only the context's name
|
|
685
|
+
/// and namespace affect the result.
|
|
686
|
+
fn parse_fragment(&self, html: &str, context: Option<u32>) -> Result<DomArena> {
|
|
687
|
+
let (context_name, namespace) = self.fragment_context(context);
|
|
535
688
|
let mut options = self.options.clone();
|
|
536
|
-
if let Some(namespace) =
|
|
537
|
-
|
|
538
|
-
.and_then(|node| node.extras.get("namespace"))
|
|
539
|
-
.cloned()
|
|
540
|
-
{
|
|
541
|
-
options["__contextNamespace"] = namespace;
|
|
689
|
+
if let Some(namespace) = namespace {
|
|
690
|
+
options["__contextNamespace"] = Value::String(namespace);
|
|
542
691
|
}
|
|
543
692
|
let document =
|
|
544
693
|
super::NativeDocument::parse_options(html, false, options, context_name.as_deref());
|
|
545
694
|
document.ensure_arena()?;
|
|
546
|
-
let
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
695
|
+
let parsed = document.arena.borrow_mut().take().unwrap();
|
|
696
|
+
Ok(parsed)
|
|
697
|
+
}
|
|
698
|
+
fn fragment_context(&self, context: Option<u32>) -> (Option<String>, Option<String>) {
|
|
699
|
+
let node = context.and_then(|id| self.node(id));
|
|
700
|
+
(
|
|
701
|
+
node.and_then(|node| node.name.clone()),
|
|
702
|
+
node.and_then(|node| node.namespace.as_deref().map(str::to_owned)),
|
|
703
|
+
)
|
|
550
704
|
}
|
|
551
705
|
pub(crate) fn set_html(&mut self, ids: &[u32], html: &str) -> Result<()> {
|
|
706
|
+
// Each target receives its own copy; targets of the same element type
|
|
707
|
+
// share one parse, since the result depends only on that context.
|
|
708
|
+
let mut parsed: HashMap<(Option<String>, Option<String>), DomArena> = HashMap::new();
|
|
552
709
|
for id in ids {
|
|
553
|
-
let
|
|
710
|
+
let key = self.fragment_context(Some(*id));
|
|
711
|
+
if !parsed.contains_key(&key) {
|
|
712
|
+
let fragment = self.parse_fragment(html, Some(*id))?;
|
|
713
|
+
parsed.insert(key.clone(), fragment);
|
|
714
|
+
}
|
|
715
|
+
let fragment = &parsed[&key];
|
|
716
|
+
let roots = fragment.nodes[0].children.clone();
|
|
717
|
+
let children = self.import_internal(&fragment.nodes, &roots, false);
|
|
554
718
|
self.set_children(*id, &children);
|
|
555
719
|
}
|
|
556
720
|
Ok(())
|
|
@@ -720,11 +884,18 @@ impl DomArena {
|
|
|
720
884
|
match field {
|
|
721
885
|
"name" | "tagName" => node.name = value.as_str().map(str::to_owned),
|
|
722
886
|
"data" | "nodeValue" => node.data = value.as_str().map(str::to_owned),
|
|
723
|
-
"type" => node.kind = value.as_str().unwrap_or("tag")
|
|
887
|
+
"type" => node.kind = node_kind(value.as_str().unwrap_or("tag")),
|
|
724
888
|
"attribs" => node.attribs = value.as_object().cloned().unwrap_or_default(),
|
|
725
889
|
"parent" => node.parent = value.as_u64().map(|id| id as u32),
|
|
726
890
|
"prev" => node.prev = value.as_u64().map(|id| id as u32),
|
|
727
891
|
"next" => node.next = value.as_u64().map(|id| id as u32),
|
|
892
|
+
"namespace" => node.namespace = value.as_str().map(namespace_name),
|
|
893
|
+
"attribsNamespace" | "x-attribsNamespace" => {
|
|
894
|
+
node.attribs_namespace = value.as_object().cloned().map(Box::new)
|
|
895
|
+
}
|
|
896
|
+
"attribsPrefix" | "x-attribsPrefix" => {
|
|
897
|
+
node.attribs_prefix = value.as_object().cloned().map(Box::new)
|
|
898
|
+
}
|
|
728
899
|
_ => {
|
|
729
900
|
if value.is_null() {
|
|
730
901
|
node.extras.remove(field);
|
|
@@ -739,7 +910,7 @@ impl DomArena {
|
|
|
739
910
|
|
|
740
911
|
impl super::selector::SelectDom for DomArena {
|
|
741
912
|
fn kind(&self, id: u32) -> &str {
|
|
742
|
-
self.node(id).map_or("", |node| node.kind.
|
|
913
|
+
self.node(id).map_or("", |node| node.kind.as_ref())
|
|
743
914
|
}
|
|
744
915
|
fn name(&self, id: u32) -> Option<&str> {
|
|
745
916
|
self.node(id)?.name.as_deref()
|
|
@@ -782,6 +953,13 @@ impl super::selector::SelectDom for DomArena {
|
|
|
782
953
|
fn pseudo_alias(&self, name: &str) -> Option<&str> {
|
|
783
954
|
self.options.get("pseudos")?.get(name)?.as_str()
|
|
784
955
|
}
|
|
956
|
+
fn elements_named(&self, lowercase_name: &str) -> Option<&[u32]> {
|
|
957
|
+
let index = self.name_index.as_ref()?;
|
|
958
|
+
Some(index.get(lowercase_name).map_or(&[], Vec::as_slice))
|
|
959
|
+
}
|
|
960
|
+
fn is_id_order_document_order(&self) -> bool {
|
|
961
|
+
self.name_index.is_some()
|
|
962
|
+
}
|
|
785
963
|
}
|
|
786
964
|
|
|
787
965
|
pub(crate) fn value_ids(value: &Value) -> Vec<u32> {
|
|
@@ -880,481 +1058,30 @@ fn escape(value: &str, attribute: bool, xml: bool, utf8: bool, output: &mut Stri
|
|
|
880
1058
|
}
|
|
881
1059
|
|
|
882
1060
|
pub(crate) fn parse_xml(source: &str, options: Value) -> DomArena {
|
|
883
|
-
|
|
884
|
-
let mut parents = vec![0u32];
|
|
885
|
-
parse_xml_segment(source, &mut arena, &mut parents, 0);
|
|
886
|
-
arena
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
fn parse_xml_segment(source: &str, arena: &mut DomArena, parents: &mut Vec<u32>, base: usize) {
|
|
890
|
-
let xml = arena.xml_mode();
|
|
891
|
-
let indices = arena.options.get("withStartIndices") == Some(&Value::Bool(true))
|
|
892
|
-
|| arena.options.get("withEndIndices") == Some(&Value::Bool(true));
|
|
893
|
-
let mut positions = if indices {
|
|
894
|
-
vec![0usize; source.len() + 1]
|
|
895
|
-
} else {
|
|
896
|
-
Vec::new()
|
|
897
|
-
};
|
|
898
|
-
let mut utf16 = 0;
|
|
899
|
-
let codec = arena.options.get("__utf16Codec") == Some(&Value::Bool(true));
|
|
900
|
-
let mut escaped = false;
|
|
901
|
-
if indices {
|
|
902
|
-
for (at, ch) in source.char_indices() {
|
|
903
|
-
for position in &mut positions[at..at + ch.len_utf8()] {
|
|
904
|
-
*position = utf16;
|
|
905
|
-
}
|
|
906
|
-
if codec && ch == crate::codec::ESCAPE && !escaped {
|
|
907
|
-
escaped = true;
|
|
908
|
-
} else {
|
|
909
|
-
utf16 += ch.len_utf16();
|
|
910
|
-
escaped = false;
|
|
911
|
-
}
|
|
912
|
-
positions[at + ch.len_utf8()] = utf16;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
let source_position = |index: usize| {
|
|
916
|
-
base + if indices {
|
|
917
|
-
positions[index.min(source.len())]
|
|
918
|
-
} else {
|
|
919
|
-
index
|
|
920
|
-
}
|
|
921
|
-
};
|
|
922
|
-
macro_rules! append_parsed {
|
|
923
|
-
($arena:expr,$parent:expr,$node:expr,$start:expr,$end:expr $(,)?) => {
|
|
924
|
-
append_parsed(
|
|
925
|
-
$arena,
|
|
926
|
-
$parent,
|
|
927
|
-
$node,
|
|
928
|
-
source_position($start),
|
|
929
|
-
source_position(($end).saturating_add(1)).saturating_sub(1),
|
|
930
|
-
)
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
let mut index = 0;
|
|
934
|
-
let bytes = source.as_bytes();
|
|
935
|
-
let decode = arena.options.get("decodeEntities") != Some(&Value::Bool(false));
|
|
936
|
-
while index < bytes.len() {
|
|
937
|
-
let start = index;
|
|
938
|
-
if !xml {
|
|
939
|
-
if let Some(name) = parents
|
|
940
|
-
.last()
|
|
941
|
-
.and_then(|id| arena.node(*id))
|
|
942
|
-
.and_then(|node| node.name.as_deref())
|
|
943
|
-
.filter(|name| matches!(*name, "script" | "style" | "xmp" | "textarea" | "title"))
|
|
944
|
-
.map(str::to_owned)
|
|
945
|
-
{
|
|
946
|
-
let closing = format!("</{name}");
|
|
947
|
-
let end = source.as_bytes()[index..]
|
|
948
|
-
.windows(closing.len())
|
|
949
|
-
.enumerate()
|
|
950
|
-
.find(|(offset, candidate)| {
|
|
951
|
-
candidate.eq_ignore_ascii_case(closing.as_bytes())
|
|
952
|
-
&& bytes
|
|
953
|
-
.get(index + offset + closing.len())
|
|
954
|
-
.is_none_or(|ch| *ch == b'>' || ch.is_ascii_whitespace())
|
|
955
|
-
})
|
|
956
|
-
.map_or(bytes.len(), |(offset, _)| index + offset);
|
|
957
|
-
if end > index {
|
|
958
|
-
let text = &source[index..end];
|
|
959
|
-
let mut node = DomNode::new("text");
|
|
960
|
-
node.data = Some(if decode && name == "title" {
|
|
961
|
-
decode_entities_mode(
|
|
962
|
-
text,
|
|
963
|
-
false,
|
|
964
|
-
false,
|
|
965
|
-
arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
|
|
966
|
-
)
|
|
967
|
-
} else {
|
|
968
|
-
text.into()
|
|
969
|
-
});
|
|
970
|
-
append_parsed!(
|
|
971
|
-
arena,
|
|
972
|
-
*parents.last().unwrap(),
|
|
973
|
-
node,
|
|
974
|
-
index,
|
|
975
|
-
end.saturating_sub(1),
|
|
976
|
-
);
|
|
977
|
-
index = end;
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
if bytes[index] == b'<'
|
|
982
|
-
&& bytes.get(index + 1).is_some_and(|ch| {
|
|
983
|
-
!ch.is_ascii_alphabetic() && !matches!(*ch, b'!' | b'?' | b'/')
|
|
984
|
-
})
|
|
985
|
-
{
|
|
986
|
-
let mut node = DomNode::new("text");
|
|
987
|
-
node.data = Some("<".into());
|
|
988
|
-
append_parsed!(arena, *parents.last().unwrap(), node, index, index);
|
|
989
|
-
index += 1;
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
if bytes[index] != b'<' {
|
|
994
|
-
index = source[index..]
|
|
995
|
-
.find('<')
|
|
996
|
-
.map_or(bytes.len(), |offset| index + offset);
|
|
997
|
-
let text = &source[start..index];
|
|
998
|
-
let mut node = DomNode::new("text");
|
|
999
|
-
node.data = Some(if decode {
|
|
1000
|
-
decode_entities_mode(
|
|
1001
|
-
text,
|
|
1002
|
-
xml,
|
|
1003
|
-
false,
|
|
1004
|
-
arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
|
|
1005
|
-
)
|
|
1006
|
-
} else {
|
|
1007
|
-
text.into()
|
|
1008
|
-
});
|
|
1009
|
-
append_parsed!(
|
|
1010
|
-
arena,
|
|
1011
|
-
*parents.last().unwrap(),
|
|
1012
|
-
node,
|
|
1013
|
-
start,
|
|
1014
|
-
index.saturating_sub(1),
|
|
1015
|
-
);
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
if source[index..].starts_with("<!--") {
|
|
1019
|
-
let end = source[index + 4..]
|
|
1020
|
-
.find("-->")
|
|
1021
|
-
.map(|offset| index + 4 + offset);
|
|
1022
|
-
let data_end = end.unwrap_or(bytes.len());
|
|
1023
|
-
index = end.map_or(bytes.len(), |end| end + 3);
|
|
1024
|
-
let mut node = DomNode::new("comment");
|
|
1025
|
-
node.data = Some(source[start + 4..data_end].into());
|
|
1026
|
-
append_parsed!(
|
|
1027
|
-
arena,
|
|
1028
|
-
*parents.last().unwrap(),
|
|
1029
|
-
node,
|
|
1030
|
-
start,
|
|
1031
|
-
index.saturating_sub(1),
|
|
1032
|
-
);
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
if source[index..].starts_with("<![CDATA[") {
|
|
1036
|
-
let end = source[index + 9..]
|
|
1037
|
-
.find("]]>")
|
|
1038
|
-
.map(|offset| index + 9 + offset);
|
|
1039
|
-
let data_end = end.unwrap_or(bytes.len());
|
|
1040
|
-
index = end.map_or(bytes.len(), |end| end + 3);
|
|
1041
|
-
if !xml && arena.options.get("recognizeCDATA") != Some(&Value::Bool(true)) {
|
|
1042
|
-
let mut node = DomNode::new("comment");
|
|
1043
|
-
node.data = Some(format!("[CDATA[{}]]", &source[start + 9..data_end]));
|
|
1044
|
-
append_parsed!(
|
|
1045
|
-
arena,
|
|
1046
|
-
*parents.last().unwrap(),
|
|
1047
|
-
node,
|
|
1048
|
-
start,
|
|
1049
|
-
index.saturating_sub(1)
|
|
1050
|
-
);
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
let id = append_parsed!(
|
|
1054
|
-
arena,
|
|
1055
|
-
*parents.last().unwrap(),
|
|
1056
|
-
DomNode::new("cdata"),
|
|
1057
|
-
start,
|
|
1058
|
-
index.saturating_sub(1),
|
|
1059
|
-
);
|
|
1060
|
-
let mut text = DomNode::new("text");
|
|
1061
|
-
text.data = Some(source[start + 9..data_end].into());
|
|
1062
|
-
append_parsed!(arena, id, text, start + 9, data_end.saturating_sub(1));
|
|
1063
|
-
continue;
|
|
1064
|
-
}
|
|
1065
|
-
let mut cursor = index + 1;
|
|
1066
|
-
let mut quote = None;
|
|
1067
|
-
while cursor < bytes.len() {
|
|
1068
|
-
let ch = bytes[cursor];
|
|
1069
|
-
if let Some(q) = quote {
|
|
1070
|
-
if ch == q {
|
|
1071
|
-
quote = None;
|
|
1072
|
-
}
|
|
1073
|
-
} else if ch == b'\'' || ch == b'"' {
|
|
1074
|
-
quote = Some(ch);
|
|
1075
|
-
} else if ch == b'>' {
|
|
1076
|
-
break;
|
|
1077
|
-
}
|
|
1078
|
-
cursor += 1;
|
|
1079
|
-
}
|
|
1080
|
-
if cursor >= bytes.len() {
|
|
1081
|
-
let mut text = DomNode::new("text");
|
|
1082
|
-
text.data = Some(source[index..].into());
|
|
1083
|
-
append_parsed!(
|
|
1084
|
-
arena,
|
|
1085
|
-
*parents.last().unwrap(),
|
|
1086
|
-
text,
|
|
1087
|
-
start,
|
|
1088
|
-
bytes.len().saturating_sub(1),
|
|
1089
|
-
);
|
|
1090
|
-
break;
|
|
1091
|
-
}
|
|
1092
|
-
index = cursor + 1;
|
|
1093
|
-
let token = source[start + 1..cursor].trim();
|
|
1094
|
-
if token.starts_with('!') || token.starts_with('?') {
|
|
1095
|
-
let mut node = DomNode::new("directive");
|
|
1096
|
-
node.name = Some(token.split_whitespace().next().unwrap_or("").into());
|
|
1097
|
-
node.data = Some(token.into());
|
|
1098
|
-
append_parsed!(arena, *parents.last().unwrap(), node, start, cursor);
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
if let Some(close) = token.strip_prefix('/') {
|
|
1102
|
-
let close = close.split_whitespace().next().unwrap_or("");
|
|
1103
|
-
if let Some(position) = parents.iter().rposition(|id| {
|
|
1104
|
-
arena.nodes[*id as usize]
|
|
1105
|
-
.name
|
|
1106
|
-
.as_deref()
|
|
1107
|
-
.is_some_and(|name| {
|
|
1108
|
-
if xml {
|
|
1109
|
-
name == close
|
|
1110
|
-
} else {
|
|
1111
|
-
name.eq_ignore_ascii_case(close)
|
|
1112
|
-
}
|
|
1113
|
-
})
|
|
1114
|
-
}) {
|
|
1115
|
-
for id in parents.drain(position..) {
|
|
1116
|
-
if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
1117
|
-
arena.nodes[id as usize]
|
|
1118
|
-
.extras
|
|
1119
|
-
.insert("endIndex".into(), json!(source_position(cursor)));
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
let slash = token.ends_with('/');
|
|
1126
|
-
let content = token.strip_suffix('/').unwrap_or(token);
|
|
1127
|
-
let name_end = content.find(char::is_whitespace).unwrap_or(content.len());
|
|
1128
|
-
let name = &content[..name_end];
|
|
1129
|
-
if name.is_empty() {
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
let lower = arena
|
|
1133
|
-
.options
|
|
1134
|
-
.get("lowerCaseTags")
|
|
1135
|
-
.and_then(Value::as_bool)
|
|
1136
|
-
.unwrap_or(!xml);
|
|
1137
|
-
let name = if lower {
|
|
1138
|
-
name.to_ascii_lowercase()
|
|
1139
|
-
} else {
|
|
1140
|
-
name.into()
|
|
1141
|
-
};
|
|
1142
|
-
if !xml {
|
|
1143
|
-
while parents
|
|
1144
|
-
.last()
|
|
1145
|
-
.and_then(|id| arena.node(*id))
|
|
1146
|
-
.and_then(|node| node.name.as_deref())
|
|
1147
|
-
.is_some_and(|open| implied_close(&name, open))
|
|
1148
|
-
{
|
|
1149
|
-
let id = parents.pop().unwrap();
|
|
1150
|
-
if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
1151
|
-
arena.nodes[id as usize].extras.insert(
|
|
1152
|
-
"endIndex".into(),
|
|
1153
|
-
json!(source_position(start.saturating_sub(1))),
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
let foreign = matches!(name.as_str(), "svg" | "math")
|
|
1159
|
-
|| parents
|
|
1160
|
-
.iter()
|
|
1161
|
-
.rev()
|
|
1162
|
-
.filter_map(|id| arena.node(*id))
|
|
1163
|
-
.filter_map(|node| node.name.as_deref())
|
|
1164
|
-
.find_map(|name| {
|
|
1165
|
-
if matches!(
|
|
1166
|
-
name,
|
|
1167
|
-
"mi" | "mo"
|
|
1168
|
-
| "mn"
|
|
1169
|
-
| "ms"
|
|
1170
|
-
| "mtext"
|
|
1171
|
-
| "annotation-xml"
|
|
1172
|
-
| "foreignobject"
|
|
1173
|
-
| "desc"
|
|
1174
|
-
| "title"
|
|
1175
|
-
) {
|
|
1176
|
-
Some(false)
|
|
1177
|
-
} else if matches!(name, "svg" | "math") {
|
|
1178
|
-
Some(true)
|
|
1179
|
-
} else {
|
|
1180
|
-
None
|
|
1181
|
-
}
|
|
1182
|
-
})
|
|
1183
|
-
.unwrap_or(false);
|
|
1184
|
-
let self_close = slash
|
|
1185
|
-
&& (xml
|
|
1186
|
-
|| foreign
|
|
1187
|
-
|| arena.options.get("recognizeSelfClosing") == Some(&Value::Bool(true)));
|
|
1188
|
-
let mut node = DomNode::new(if !xml && name == "script" {
|
|
1189
|
-
"script"
|
|
1190
|
-
} else if !xml && name == "style" {
|
|
1191
|
-
"style"
|
|
1192
|
-
} else {
|
|
1193
|
-
"tag"
|
|
1194
|
-
});
|
|
1195
|
-
node.name = Some(name.clone());
|
|
1196
|
-
node.attribs = parse_attributes(
|
|
1197
|
-
&content[name_end..],
|
|
1198
|
-
decode,
|
|
1199
|
-
arena
|
|
1200
|
-
.options
|
|
1201
|
-
.get("lowerCaseAttributeNames")
|
|
1202
|
-
.and_then(Value::as_bool)
|
|
1203
|
-
.unwrap_or(!xml),
|
|
1204
|
-
xml,
|
|
1205
|
-
arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
|
|
1206
|
-
);
|
|
1207
|
-
let id = append_parsed!(arena, *parents.last().unwrap(), node, start, cursor);
|
|
1208
|
-
let void = !xml && html_parser_void(&name);
|
|
1209
|
-
if !self_close && !void {
|
|
1210
|
-
parents.push(id);
|
|
1211
|
-
} else if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
|
|
1212
|
-
arena.nodes[id as usize]
|
|
1213
|
-
.extras
|
|
1214
|
-
.insert("endIndex".into(), json!(source_position(cursor)));
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1061
|
+
crate::tokenizer::parse_markup(source, options)
|
|
1217
1062
|
}
|
|
1218
1063
|
|
|
1219
|
-
/// Incremental
|
|
1220
|
-
///
|
|
1064
|
+
/// Incremental htmlparser2/XML input. The complete input is tokenized once, so
|
|
1065
|
+
/// tags and entities split across chunks parse exactly as one string does.
|
|
1221
1066
|
pub(crate) struct XmlStream {
|
|
1222
1067
|
pub arena: DomArena,
|
|
1223
|
-
parents: Vec<u32>,
|
|
1224
1068
|
pending: String,
|
|
1225
|
-
offset: usize,
|
|
1226
1069
|
}
|
|
1227
1070
|
impl XmlStream {
|
|
1228
1071
|
pub(crate) fn new(options: Value) -> Self {
|
|
1229
1072
|
Self {
|
|
1230
1073
|
arena: DomArena::from_records(vec![DomNode::new("root")], options),
|
|
1231
|
-
parents: vec![0],
|
|
1232
1074
|
pending: String::new(),
|
|
1233
|
-
offset: 0,
|
|
1234
1075
|
}
|
|
1235
1076
|
}
|
|
1236
1077
|
pub(crate) fn feed(&mut self, chunk: &str, finish: bool) {
|
|
1237
1078
|
self.pending.push_str(chunk);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
let mut length = if finish {
|
|
1242
|
-
input.len()
|
|
1243
|
-
} else {
|
|
1244
|
-
self.complete_prefix(input)
|
|
1245
|
-
};
|
|
1246
|
-
if !finish
|
|
1247
|
-
&& self.arena.options.get("__utf16Codec") == Some(&Value::Bool(true))
|
|
1248
|
-
&& input[..length]
|
|
1249
|
-
.chars()
|
|
1250
|
-
.rev()
|
|
1251
|
-
.take_while(|ch| *ch == crate::codec::ESCAPE)
|
|
1252
|
-
.count()
|
|
1253
|
-
% 2
|
|
1254
|
-
== 1
|
|
1255
|
-
{
|
|
1256
|
-
length -= crate::codec::ESCAPE.len_utf8();
|
|
1257
|
-
}
|
|
1258
|
-
if length == 0 {
|
|
1259
|
-
break;
|
|
1260
|
-
}
|
|
1261
|
-
let segment = &input[..length];
|
|
1262
|
-
parse_xml_segment(segment, &mut self.arena, &mut self.parents, self.offset);
|
|
1263
|
-
self.offset += if self.arena.options.get("__utf16Codec") == Some(&Value::Bool(true)) {
|
|
1264
|
-
crate::codec::logical_len(segment)
|
|
1265
|
-
} else {
|
|
1266
|
-
segment.encode_utf16().count()
|
|
1267
|
-
};
|
|
1268
|
-
consumed += length;
|
|
1269
|
-
if consumed == self.pending.len() || finish {
|
|
1270
|
-
break;
|
|
1271
|
-
}
|
|
1079
|
+
if finish {
|
|
1080
|
+
let options = self.arena.options.clone();
|
|
1081
|
+
self.arena = parse_xml(&std::mem::take(&mut self.pending), options);
|
|
1272
1082
|
}
|
|
1273
|
-
self.pending.drain(..consumed);
|
|
1274
|
-
}
|
|
1275
|
-
fn complete_prefix(&self, input: &str) -> usize {
|
|
1276
|
-
let bytes = input.as_bytes();
|
|
1277
|
-
if input.is_empty() {
|
|
1278
|
-
return 0;
|
|
1279
|
-
}
|
|
1280
|
-
if !self.arena.xml_mode() {
|
|
1281
|
-
if let Some(name) = self
|
|
1282
|
-
.parents
|
|
1283
|
-
.last()
|
|
1284
|
-
.and_then(|id| self.arena.node(*id))
|
|
1285
|
-
.and_then(|node| node.name.as_deref())
|
|
1286
|
-
.filter(|name| matches!(*name, "script" | "style" | "xmp" | "textarea" | "title"))
|
|
1287
|
-
{
|
|
1288
|
-
let close = format!("</{name}");
|
|
1289
|
-
let closing = bytes
|
|
1290
|
-
.windows(close.len())
|
|
1291
|
-
.enumerate()
|
|
1292
|
-
.find(|(at, value)| {
|
|
1293
|
-
value.eq_ignore_ascii_case(close.as_bytes())
|
|
1294
|
-
&& bytes
|
|
1295
|
-
.get(at + close.len())
|
|
1296
|
-
.is_none_or(|ch| *ch == b'>' || ch.is_ascii_whitespace())
|
|
1297
|
-
})
|
|
1298
|
-
.map(|(at, _)| at);
|
|
1299
|
-
match closing {
|
|
1300
|
-
Some(0) if bytes.len() == close.len() => return 0,
|
|
1301
|
-
Some(0) => {}
|
|
1302
|
-
Some(index) => return safe_entity_prefix(input, index, name == "title"),
|
|
1303
|
-
None => {
|
|
1304
|
-
let mut end = input.len().saturating_sub(close.len() + 1);
|
|
1305
|
-
while !input.is_char_boundary(end) {
|
|
1306
|
-
end -= 1;
|
|
1307
|
-
}
|
|
1308
|
-
return safe_entity_prefix(input, end, name == "title");
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
if bytes[0] != b'<' {
|
|
1314
|
-
let end = input.find('<').unwrap_or(input.len());
|
|
1315
|
-
return safe_entity_prefix(
|
|
1316
|
-
input,
|
|
1317
|
-
end,
|
|
1318
|
-
self.arena.options.get("decodeEntities") != Some(&Value::Bool(false)),
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
if input.starts_with("<!--") {
|
|
1322
|
-
return input.find("-->").map_or(0, |index| index + 3);
|
|
1323
|
-
}
|
|
1324
|
-
if input.starts_with("<![CDATA[") {
|
|
1325
|
-
return input.find("]]>").map_or(0, |index| index + 3);
|
|
1326
|
-
}
|
|
1327
|
-
let mut quote = None;
|
|
1328
|
-
for (index, ch) in bytes.iter().copied().enumerate().skip(1) {
|
|
1329
|
-
if let Some(q) = quote {
|
|
1330
|
-
if ch == q {
|
|
1331
|
-
quote = None;
|
|
1332
|
-
}
|
|
1333
|
-
} else if ch == b'\'' || ch == b'"' {
|
|
1334
|
-
quote = Some(ch);
|
|
1335
|
-
} else if ch == b'>' {
|
|
1336
|
-
return index + 1;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
0
|
|
1340
1083
|
}
|
|
1341
1084
|
}
|
|
1342
|
-
fn safe_entity_prefix(source: &str, end: usize, decode: bool) -> usize {
|
|
1343
|
-
if decode {
|
|
1344
|
-
if let Some(start) = source[..end].rfind('&') {
|
|
1345
|
-
let tail = &source[start + 1..end];
|
|
1346
|
-
if tail.len() < 64
|
|
1347
|
-
&& tail
|
|
1348
|
-
.chars()
|
|
1349
|
-
.all(|ch| ch.is_ascii_alphanumeric() || ch == '#')
|
|
1350
|
-
{
|
|
1351
|
-
return start;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
end
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
1085
|
pub(crate) fn append_parsed(
|
|
1359
1086
|
arena: &mut DomArena,
|
|
1360
1087
|
parent: u32,
|
|
@@ -1362,6 +1089,12 @@ pub(crate) fn append_parsed(
|
|
|
1362
1089
|
start: usize,
|
|
1363
1090
|
end: usize,
|
|
1364
1091
|
) -> u32 {
|
|
1092
|
+
let (start_indices, end_indices) = *arena.parse_indices.get_or_insert_with(|| {
|
|
1093
|
+
(
|
|
1094
|
+
arena.options.get("withStartIndices") == Some(&Value::Bool(true)),
|
|
1095
|
+
arena.options.get("withEndIndices") == Some(&Value::Bool(true)),
|
|
1096
|
+
)
|
|
1097
|
+
});
|
|
1365
1098
|
if node.kind == "text" {
|
|
1366
1099
|
if let Some(previous) = arena.nodes[parent as usize]
|
|
1367
1100
|
.children
|
|
@@ -1373,7 +1106,7 @@ pub(crate) fn append_parsed(
|
|
|
1373
1106
|
.data
|
|
1374
1107
|
.get_or_insert_with(String::new)
|
|
1375
1108
|
.push_str(node.data.as_deref().unwrap_or(""));
|
|
1376
|
-
if
|
|
1109
|
+
if end_indices {
|
|
1377
1110
|
arena.nodes[previous as usize]
|
|
1378
1111
|
.extras
|
|
1379
1112
|
.insert("endIndex".into(), json!(end));
|
|
@@ -1381,10 +1114,10 @@ pub(crate) fn append_parsed(
|
|
|
1381
1114
|
return previous;
|
|
1382
1115
|
}
|
|
1383
1116
|
}
|
|
1384
|
-
if
|
|
1117
|
+
if start_indices {
|
|
1385
1118
|
node.extras.insert("startIndex".into(), json!(start));
|
|
1386
1119
|
}
|
|
1387
|
-
if
|
|
1120
|
+
if end_indices {
|
|
1388
1121
|
node.extras.insert("endIndex".into(), json!(end));
|
|
1389
1122
|
}
|
|
1390
1123
|
let previous = arena.nodes[parent as usize].children.last().copied();
|
|
@@ -1424,70 +1157,6 @@ pub(crate) fn implied_close(name: &str, open: &str) -> bool {
|
|
|
1424
1157
|
_ => false,
|
|
1425
1158
|
}
|
|
1426
1159
|
}
|
|
1427
|
-
fn parse_attributes(
|
|
1428
|
-
source: &str,
|
|
1429
|
-
decode: bool,
|
|
1430
|
-
lower: bool,
|
|
1431
|
-
xml: bool,
|
|
1432
|
-
codec: bool,
|
|
1433
|
-
) -> Map<String, Value> {
|
|
1434
|
-
let bytes = source.as_bytes();
|
|
1435
|
-
let mut index = 0;
|
|
1436
|
-
let mut attrs = Map::new();
|
|
1437
|
-
while index < bytes.len() {
|
|
1438
|
-
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
1439
|
-
index += 1;
|
|
1440
|
-
}
|
|
1441
|
-
let start = index;
|
|
1442
|
-
while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'=' {
|
|
1443
|
-
index += 1;
|
|
1444
|
-
}
|
|
1445
|
-
if start == index {
|
|
1446
|
-
index += 1;
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
let name = &source[start..index];
|
|
1450
|
-
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
1451
|
-
index += 1;
|
|
1452
|
-
}
|
|
1453
|
-
let mut value = "";
|
|
1454
|
-
if bytes.get(index) == Some(&b'=') {
|
|
1455
|
-
index += 1;
|
|
1456
|
-
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
1457
|
-
index += 1;
|
|
1458
|
-
}
|
|
1459
|
-
if matches!(bytes.get(index), Some(b'\'' | b'"')) {
|
|
1460
|
-
let quote = bytes[index];
|
|
1461
|
-
index += 1;
|
|
1462
|
-
let start = index;
|
|
1463
|
-
while index < bytes.len() && bytes[index] != quote {
|
|
1464
|
-
index += 1;
|
|
1465
|
-
}
|
|
1466
|
-
value = &source[start..index];
|
|
1467
|
-
index += (index < bytes.len()) as usize;
|
|
1468
|
-
} else {
|
|
1469
|
-
let start = index;
|
|
1470
|
-
while index < bytes.len() && !bytes[index].is_ascii_whitespace() {
|
|
1471
|
-
index += 1;
|
|
1472
|
-
}
|
|
1473
|
-
value = &source[start..index];
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
let name = if lower {
|
|
1477
|
-
name.to_ascii_lowercase()
|
|
1478
|
-
} else {
|
|
1479
|
-
name.into()
|
|
1480
|
-
};
|
|
1481
|
-
attrs.entry(name).or_insert_with(|| {
|
|
1482
|
-
Value::String(if decode {
|
|
1483
|
-
decode_entities_mode(value, xml, true, codec)
|
|
1484
|
-
} else {
|
|
1485
|
-
value.into()
|
|
1486
|
-
})
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
attrs
|
|
1490
|
-
}
|
|
1491
1160
|
pub(crate) fn decode_entities_mode(
|
|
1492
1161
|
source: &str,
|
|
1493
1162
|
xml: bool,
|
|
@@ -1734,47 +1403,7 @@ pub(crate) fn primitive(arena: &mut DomArena, command: &str, args: &Value) -> Re
|
|
|
1734
1403
|
}
|
|
1735
1404
|
Value::Null
|
|
1736
1405
|
}
|
|
1737
|
-
"uniqueSort" =>
|
|
1738
|
-
let mut ids = ids;
|
|
1739
|
-
let mut unique = HashSet::new();
|
|
1740
|
-
ids.retain(|id| unique.insert(*id));
|
|
1741
|
-
let ancestry = |id: u32| {
|
|
1742
|
-
let mut path = vec![id];
|
|
1743
|
-
let mut seen = HashSet::from([id]);
|
|
1744
|
-
let mut parent = arena.node(id).and_then(|node| node.parent);
|
|
1745
|
-
while let Some(id) = parent {
|
|
1746
|
-
if !seen.insert(id) {
|
|
1747
|
-
break;
|
|
1748
|
-
}
|
|
1749
|
-
path.push(id);
|
|
1750
|
-
parent = arena.node(id).and_then(|node| node.parent);
|
|
1751
|
-
}
|
|
1752
|
-
path.reverse();
|
|
1753
|
-
path
|
|
1754
|
-
};
|
|
1755
|
-
let paths: HashMap<_, _> = ids.iter().map(|id| (*id, ancestry(*id))).collect();
|
|
1756
|
-
ids.sort_by(|a, b| {
|
|
1757
|
-
use std::cmp::Ordering;
|
|
1758
|
-
let (a, b) = (&paths[a], &paths[b]);
|
|
1759
|
-
if a.first() != b.first() {
|
|
1760
|
-
return Ordering::Equal;
|
|
1761
|
-
}
|
|
1762
|
-
for index in 0..a.len().min(b.len()) {
|
|
1763
|
-
if a[index] != b[index] {
|
|
1764
|
-
let siblings = arena
|
|
1765
|
-
.node(a[index - 1])
|
|
1766
|
-
.map(|node| node.children.as_slice())
|
|
1767
|
-
.unwrap_or(&[]);
|
|
1768
|
-
return siblings
|
|
1769
|
-
.iter()
|
|
1770
|
-
.position(|id| *id == a[index])
|
|
1771
|
-
.cmp(&siblings.iter().position(|id| *id == b[index]));
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
a.len().cmp(&b.len())
|
|
1775
|
-
});
|
|
1776
|
-
json!(ids)
|
|
1777
|
-
}
|
|
1406
|
+
"uniqueSort" => json!(crate::document_order::unique_sort(&*arena, &ids)),
|
|
1778
1407
|
"contains" => {
|
|
1779
1408
|
let parent = args
|
|
1780
1409
|
.get("container")
|