rusteerio 1.0.1 → 1.1.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 +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 +527 -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
package/dist/slim.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { contains, dom, merge } from './index.mjs';
|
|
2
|
-
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-
|
|
3
|
-
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-
|
|
2
|
+
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-Cs9UP1Vv.mjs';
|
|
3
|
+
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-Cs9UP1Vv.mjs';
|
|
4
4
|
import 'node:stream';
|
|
5
5
|
import 'node:querystring';
|
|
6
6
|
import 'node:buffer';
|
package/dist/slim.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { contains, dom, merge } from './index.js';
|
|
2
|
-
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-
|
|
3
|
-
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-
|
|
2
|
+
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-Cs9UP1Vv.js';
|
|
3
|
+
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-Cs9UP1Vv.js';
|
|
4
4
|
import 'node:stream';
|
|
5
5
|
import 'node:querystring';
|
|
6
6
|
import 'node:buffer';
|
package/dist/slim.mjs
CHANGED
|
@@ -69,14 +69,14 @@ interface AttributeMethods<T = AnyNode> {
|
|
|
69
69
|
serialize<U extends AnyNode>(this: Cheerio<U>): string;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** An array-like selection of
|
|
72
|
+
/** An array-like selection of native nodes. */
|
|
73
73
|
declare class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
|
|
74
74
|
[index: number]: T;
|
|
75
|
+
length: number;
|
|
75
76
|
options: CheerioOptions;
|
|
76
77
|
_root: Cheerio<Document> | null;
|
|
77
78
|
prevObject: Cheerio<any> | undefined;
|
|
78
|
-
|
|
79
|
-
cheerio: "[cheerio object]";
|
|
79
|
+
cheerio: '[cheerio object]';
|
|
80
80
|
constructor(api: CheerioAPI, document: NativeDocument, ids?: number[], root?: Cheerio<Document> | null, options?: CheerioOptions, values?: T[]);
|
|
81
81
|
_make<R>(value: ArrayLike<R> | R | string, context?: BasicAcceptedElems<AnyNode>): Cheerio<R>;
|
|
82
82
|
_parse(content: any, options: CheerioOptions, isDocument: boolean, context: ParentNode | null): Document;
|
|
@@ -69,14 +69,14 @@ interface AttributeMethods<T = AnyNode> {
|
|
|
69
69
|
serialize<U extends AnyNode>(this: Cheerio<U>): string;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** An array-like selection of
|
|
72
|
+
/** An array-like selection of native nodes. */
|
|
73
73
|
declare class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
|
|
74
74
|
[index: number]: T;
|
|
75
|
+
length: number;
|
|
75
76
|
options: CheerioOptions;
|
|
76
77
|
_root: Cheerio<Document> | null;
|
|
77
78
|
prevObject: Cheerio<any> | undefined;
|
|
78
|
-
|
|
79
|
-
cheerio: "[cheerio object]";
|
|
79
|
+
cheerio: '[cheerio object]';
|
|
80
80
|
constructor(api: CheerioAPI, document: NativeDocument, ids?: number[], root?: Cheerio<Document> | null, options?: CheerioOptions, values?: T[]);
|
|
81
81
|
_make<R>(value: ArrayLike<R> | R | string, context?: BasicAcceptedElems<AnyNode>): Cheerio<R>;
|
|
82
82
|
_parse(content: any, options: CheerioOptions, isDocument: boolean, context: ParentNode | null): Document;
|
package/dist/utils.d.mts
CHANGED
package/dist/utils.d.ts
CHANGED
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,12 +512,10 @@ 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
|
-
});
|
|
515
|
+
.then_some((
|
|
516
|
+
node.attribs_namespace.as_deref(),
|
|
517
|
+
node.attribs_prefix.as_deref(),
|
|
518
|
+
));
|
|
375
519
|
let attributes = super::js_property_order(
|
|
376
520
|
node.attribs
|
|
377
521
|
.iter()
|
|
@@ -420,9 +564,8 @@ impl DomArena {
|
|
|
420
564
|
}
|
|
421
565
|
let is_void = !xml
|
|
422
566
|
&& (node
|
|
423
|
-
.
|
|
424
|
-
.
|
|
425
|
-
.and_then(Value::as_str)
|
|
567
|
+
.namespace
|
|
568
|
+
.as_deref()
|
|
426
569
|
.is_none_or(|namespace| namespace == super::HTML_NS))
|
|
427
570
|
&& super::void_name(name);
|
|
428
571
|
if xml
|
|
@@ -534,28 +677,44 @@ impl DomArena {
|
|
|
534
677
|
roots
|
|
535
678
|
}
|
|
536
679
|
pub(crate) fn create_fragment(&mut self, html: &str, context: Option<u32>) -> Result<Vec<u32>> {
|
|
537
|
-
let
|
|
538
|
-
|
|
539
|
-
|
|
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);
|
|
540
688
|
let mut options = self.options.clone();
|
|
541
|
-
if let Some(namespace) =
|
|
542
|
-
|
|
543
|
-
.and_then(|node| node.extras.get("namespace"))
|
|
544
|
-
.cloned()
|
|
545
|
-
{
|
|
546
|
-
options["__contextNamespace"] = namespace;
|
|
689
|
+
if let Some(namespace) = namespace {
|
|
690
|
+
options["__contextNamespace"] = Value::String(namespace);
|
|
547
691
|
}
|
|
548
692
|
let document =
|
|
549
693
|
super::NativeDocument::parse_options(html, false, options, context_name.as_deref());
|
|
550
694
|
document.ensure_arena()?;
|
|
551
|
-
let
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
+
)
|
|
555
704
|
}
|
|
556
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();
|
|
557
709
|
for id in ids {
|
|
558
|
-
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);
|
|
559
718
|
self.set_children(*id, &children);
|
|
560
719
|
}
|
|
561
720
|
Ok(())
|
|
@@ -725,11 +884,18 @@ impl DomArena {
|
|
|
725
884
|
match field {
|
|
726
885
|
"name" | "tagName" => node.name = value.as_str().map(str::to_owned),
|
|
727
886
|
"data" | "nodeValue" => node.data = value.as_str().map(str::to_owned),
|
|
728
|
-
"type" => node.kind = value.as_str().unwrap_or("tag")
|
|
887
|
+
"type" => node.kind = node_kind(value.as_str().unwrap_or("tag")),
|
|
729
888
|
"attribs" => node.attribs = value.as_object().cloned().unwrap_or_default(),
|
|
730
889
|
"parent" => node.parent = value.as_u64().map(|id| id as u32),
|
|
731
890
|
"prev" => node.prev = value.as_u64().map(|id| id as u32),
|
|
732
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
|
+
}
|
|
733
899
|
_ => {
|
|
734
900
|
if value.is_null() {
|
|
735
901
|
node.extras.remove(field);
|
|
@@ -744,7 +910,7 @@ impl DomArena {
|
|
|
744
910
|
|
|
745
911
|
impl super::selector::SelectDom for DomArena {
|
|
746
912
|
fn kind(&self, id: u32) -> &str {
|
|
747
|
-
self.node(id).map_or("", |node| node.kind.
|
|
913
|
+
self.node(id).map_or("", |node| node.kind.as_ref())
|
|
748
914
|
}
|
|
749
915
|
fn name(&self, id: u32) -> Option<&str> {
|
|
750
916
|
self.node(id)?.name.as_deref()
|
|
@@ -787,6 +953,13 @@ impl super::selector::SelectDom for DomArena {
|
|
|
787
953
|
fn pseudo_alias(&self, name: &str) -> Option<&str> {
|
|
788
954
|
self.options.get("pseudos")?.get(name)?.as_str()
|
|
789
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
|
+
}
|
|
790
963
|
}
|
|
791
964
|
|
|
792
965
|
pub(crate) fn value_ids(value: &Value) -> Vec<u32> {
|
|
@@ -916,6 +1089,12 @@ pub(crate) fn append_parsed(
|
|
|
916
1089
|
start: usize,
|
|
917
1090
|
end: usize,
|
|
918
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
|
+
});
|
|
919
1098
|
if node.kind == "text" {
|
|
920
1099
|
if let Some(previous) = arena.nodes[parent as usize]
|
|
921
1100
|
.children
|
|
@@ -927,7 +1106,7 @@ pub(crate) fn append_parsed(
|
|
|
927
1106
|
.data
|
|
928
1107
|
.get_or_insert_with(String::new)
|
|
929
1108
|
.push_str(node.data.as_deref().unwrap_or(""));
|
|
930
|
-
if
|
|
1109
|
+
if end_indices {
|
|
931
1110
|
arena.nodes[previous as usize]
|
|
932
1111
|
.extras
|
|
933
1112
|
.insert("endIndex".into(), json!(end));
|
|
@@ -935,10 +1114,10 @@ pub(crate) fn append_parsed(
|
|
|
935
1114
|
return previous;
|
|
936
1115
|
}
|
|
937
1116
|
}
|
|
938
|
-
if
|
|
1117
|
+
if start_indices {
|
|
939
1118
|
node.extras.insert("startIndex".into(), json!(start));
|
|
940
1119
|
}
|
|
941
|
-
if
|
|
1120
|
+
if end_indices {
|
|
942
1121
|
node.extras.insert("endIndex".into(), json!(end));
|
|
943
1122
|
}
|
|
944
1123
|
let previous = arena.nodes[parent as usize].children.last().copied();
|
|
@@ -1224,47 +1403,7 @@ pub(crate) fn primitive(arena: &mut DomArena, command: &str, args: &Value) -> Re
|
|
|
1224
1403
|
}
|
|
1225
1404
|
Value::Null
|
|
1226
1405
|
}
|
|
1227
|
-
"uniqueSort" =>
|
|
1228
|
-
let mut ids = ids;
|
|
1229
|
-
let mut unique = HashSet::new();
|
|
1230
|
-
ids.retain(|id| unique.insert(*id));
|
|
1231
|
-
let ancestry = |id: u32| {
|
|
1232
|
-
let mut path = vec![id];
|
|
1233
|
-
let mut seen = HashSet::from([id]);
|
|
1234
|
-
let mut parent = arena.node(id).and_then(|node| node.parent);
|
|
1235
|
-
while let Some(id) = parent {
|
|
1236
|
-
if !seen.insert(id) {
|
|
1237
|
-
break;
|
|
1238
|
-
}
|
|
1239
|
-
path.push(id);
|
|
1240
|
-
parent = arena.node(id).and_then(|node| node.parent);
|
|
1241
|
-
}
|
|
1242
|
-
path.reverse();
|
|
1243
|
-
path
|
|
1244
|
-
};
|
|
1245
|
-
let paths: HashMap<_, _> = ids.iter().map(|id| (*id, ancestry(*id))).collect();
|
|
1246
|
-
ids.sort_by(|a, b| {
|
|
1247
|
-
use std::cmp::Ordering;
|
|
1248
|
-
let (a, b) = (&paths[a], &paths[b]);
|
|
1249
|
-
if a.first() != b.first() {
|
|
1250
|
-
return Ordering::Equal;
|
|
1251
|
-
}
|
|
1252
|
-
for index in 0..a.len().min(b.len()) {
|
|
1253
|
-
if a[index] != b[index] {
|
|
1254
|
-
let siblings = arena
|
|
1255
|
-
.node(a[index - 1])
|
|
1256
|
-
.map(|node| node.children.as_slice())
|
|
1257
|
-
.unwrap_or(&[]);
|
|
1258
|
-
return siblings
|
|
1259
|
-
.iter()
|
|
1260
|
-
.position(|id| *id == a[index])
|
|
1261
|
-
.cmp(&siblings.iter().position(|id| *id == b[index]));
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
a.len().cmp(&b.len())
|
|
1265
|
-
});
|
|
1266
|
-
json!(ids)
|
|
1267
|
-
}
|
|
1406
|
+
"uniqueSort" => json!(crate::document_order::unique_sort(&*arena, &ids)),
|
|
1268
1407
|
"contains" => {
|
|
1269
1408
|
let parent = args
|
|
1270
1409
|
.get("container")
|
package/native/src/attributes.rs
CHANGED
|
@@ -173,7 +173,7 @@ fn set_css(arena: &mut DomArena, ids: &[u32], name: &str, value: &Value) {
|
|
|
173
173
|
}
|
|
174
174
|
let mut properties = parse_css(arena.attr(*id, "style").unwrap_or(""));
|
|
175
175
|
if value.as_str() == Some("") {
|
|
176
|
-
properties.
|
|
176
|
+
properties.shift_remove(name);
|
|
177
177
|
} else if !value.is_null() {
|
|
178
178
|
properties.insert(name.into(), json!(js_string(value)));
|
|
179
179
|
}
|
|
@@ -207,9 +207,15 @@ fn node_property(arena: &DomArena, id: u32, name: &str) -> bool {
|
|
|
207
207
|
| "type"
|
|
208
208
|
| "startIndex"
|
|
209
209
|
| "endIndex"
|
|
210
|
-
) || arena
|
|
211
|
-
.
|
|
212
|
-
|
|
210
|
+
) || arena.node(id).is_some_and(|node| {
|
|
211
|
+
node.extras.contains_key(name)
|
|
212
|
+
|| match name {
|
|
213
|
+
"namespace" => node.namespace.is_some(),
|
|
214
|
+
"attribsNamespace" => node.attribs_namespace.is_some(),
|
|
215
|
+
"attribsPrefix" => node.attribs_prefix.is_some(),
|
|
216
|
+
_ => false,
|
|
217
|
+
}
|
|
218
|
+
})
|
|
213
219
|
}
|
|
214
220
|
|
|
215
221
|
fn get_prop(arena: &DomArena, id: u32, name: &str) -> napi::Result<Value> {
|
|
@@ -682,7 +688,7 @@ pub(crate) fn execute(
|
|
|
682
688
|
.and_then(|node| node.extras.get_mut("data"))
|
|
683
689
|
.and_then(Value::as_object_mut)
|
|
684
690
|
{
|
|
685
|
-
data.
|
|
691
|
+
data.shift_remove(name);
|
|
686
692
|
}
|
|
687
693
|
}
|
|
688
694
|
}
|