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/lib.rs
CHANGED
|
@@ -14,18 +14,27 @@ use napi_derive::napi;
|
|
|
14
14
|
use scraper::{Html, Node, Selector};
|
|
15
15
|
use serde_json::{Map, Value};
|
|
16
16
|
|
|
17
|
+
#[cfg(target_os = "linux")]
|
|
18
|
+
#[global_allocator]
|
|
19
|
+
static ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
20
|
+
|
|
17
21
|
mod arena;
|
|
18
22
|
mod attributes;
|
|
19
23
|
mod codec;
|
|
20
24
|
mod diagnostics;
|
|
25
|
+
mod document_order;
|
|
21
26
|
mod extraction;
|
|
22
27
|
mod gc;
|
|
23
28
|
mod hook_sink;
|
|
24
29
|
mod hook_stream;
|
|
30
|
+
mod id_hash;
|
|
31
|
+
mod immutable;
|
|
25
32
|
mod location;
|
|
33
|
+
mod markup_tokenizer;
|
|
26
34
|
mod runtime;
|
|
27
35
|
mod selector;
|
|
28
36
|
mod sink;
|
|
37
|
+
mod snapshot;
|
|
29
38
|
mod stream;
|
|
30
39
|
mod tokenizer;
|
|
31
40
|
mod tracker;
|
|
@@ -68,19 +77,81 @@ pub(crate) fn push_attribute_name(
|
|
|
68
77
|
output.push_str(local);
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
/// Orders attributes the way a JavaScript object enumerates its keys: integer
|
|
81
|
+
/// keys first, ascending, then insertion order. Cheerio serializes attributes
|
|
82
|
+
/// from such an object.
|
|
83
|
+
pub(crate) fn js_property_order<'a, T, I>(entries: I) -> JsPropertyOrder<I>
|
|
84
|
+
where
|
|
85
|
+
I: Iterator<Item = (&'a str, T)> + Clone,
|
|
86
|
+
{
|
|
87
|
+
if !entries.clone().any(|(name, _)| array_index(name).is_some()) {
|
|
88
|
+
return JsPropertyOrder::Unchanged(entries);
|
|
89
|
+
}
|
|
90
|
+
let mut keyed: Vec<_> = entries
|
|
91
|
+
.map(|entry| {
|
|
92
|
+
(
|
|
93
|
+
array_index(entry.0).map_or((1, 0), |index| (0, index)),
|
|
94
|
+
entry,
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
.collect();
|
|
98
|
+
keyed.sort_by_key(|(key, _)| *key);
|
|
99
|
+
let sorted: Vec<_> = keyed.into_iter().map(|(_, entry)| entry).collect();
|
|
100
|
+
JsPropertyOrder::Sorted(sorted.into_iter())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub(crate) enum JsPropertyOrder<I: Iterator> {
|
|
104
|
+
Unchanged(I),
|
|
105
|
+
Sorted(std::vec::IntoIter<I::Item>),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
impl<I: Iterator> Iterator for JsPropertyOrder<I> {
|
|
109
|
+
type Item = I::Item;
|
|
110
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
111
|
+
match self {
|
|
112
|
+
Self::Unchanged(entries) => entries.next(),
|
|
113
|
+
Self::Sorted(entries) => entries.next(),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn array_index(name: &str) -> Option<u32> {
|
|
119
|
+
let is_canonical = !name.is_empty()
|
|
120
|
+
&& name.len() <= 10
|
|
121
|
+
&& name.bytes().all(|byte| byte.is_ascii_digit())
|
|
122
|
+
&& (name == "0" || !name.starts_with('0'));
|
|
123
|
+
is_canonical
|
|
124
|
+
.then(|| name.parse::<u64>().ok())
|
|
125
|
+
.flatten()
|
|
126
|
+
.filter(|value| *value < u64::from(u32::MAX))
|
|
127
|
+
.map(|value| value as u32)
|
|
128
|
+
}
|
|
129
|
+
|
|
71
130
|
#[napi(custom_finalize)]
|
|
72
131
|
pub struct NativeDocument {
|
|
73
132
|
html: RefCell<Option<Html>>,
|
|
74
133
|
// Stable public IDs are preorder positions, rather than parser allocation
|
|
75
134
|
// positions: HTML5 foster parenting can change the latter's ordering.
|
|
76
135
|
ids: RefCell<Vec<NodeId>>,
|
|
77
|
-
reverse: RefCell<
|
|
136
|
+
reverse: RefCell<id_hash::IdMap<NodeId, u32>>,
|
|
137
|
+
/// Parent/child links of the parse tree, built on the first query that needs them.
|
|
138
|
+
tree_index: RefCell<Option<immutable::TreeIndex>>,
|
|
78
139
|
pub(crate) arena: RefCell<Option<arena::DomArena>>,
|
|
79
140
|
options: Value,
|
|
80
141
|
source: RefCell<Option<String>>,
|
|
81
142
|
retained: RefCell<HashMap<u32, usize>>,
|
|
82
143
|
needs_collect: Cell<bool>,
|
|
83
144
|
managed: Cell<bool>,
|
|
145
|
+
/// Slots managed collections keep regardless of pins.
|
|
146
|
+
kept_slots: Cell<u32>,
|
|
147
|
+
/// The document index JavaScript routes this document's node ids by.
|
|
148
|
+
routing_index: Cell<Option<u32>>,
|
|
149
|
+
/// Nodes were removed from the parse tree, so traversal order no longer
|
|
150
|
+
/// matches local ids and parsed nodes may be orphans.
|
|
151
|
+
tree_detached: Cell<bool>,
|
|
152
|
+
/// Parents whose children a parse-tree removal changed since JavaScript last asked.
|
|
153
|
+
tree_touched: RefCell<Vec<u32>>,
|
|
154
|
+
_live: LiveDocument,
|
|
84
155
|
diagnostics: RefCell<Vec<Value>>,
|
|
85
156
|
locations: RefCell<HashMap<u32, Value>>,
|
|
86
157
|
// V8 cannot see native allocations. Reporting them lets its GC collect
|
|
@@ -89,12 +160,71 @@ pub struct NativeDocument {
|
|
|
89
160
|
reported_heap_bytes: Cell<i64>,
|
|
90
161
|
}
|
|
91
162
|
|
|
163
|
+
/// Node-API calls the finalizers of a module that declares the experimental
|
|
164
|
+
/// version directly from garbage collection, rather than on a later turn of
|
|
165
|
+
/// the event loop, so a synchronous loop over documents frees each native tree
|
|
166
|
+
/// as soon as V8 collects its document. Finalizers must therefore not call
|
|
167
|
+
/// into V8: see `NativeDocument::finalize`.
|
|
168
|
+
#[no_mangle]
|
|
169
|
+
pub extern "C" fn node_api_module_get_api_version_v1() -> i32 {
|
|
170
|
+
NAPI_VERSION_EXPERIMENTAL
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const NAPI_VERSION_EXPERIMENTAL: i32 = i32::MAX;
|
|
174
|
+
|
|
175
|
+
thread_local! {
|
|
176
|
+
/// External memory of finalized documents not yet returned to V8.
|
|
177
|
+
static RELEASED_HEAP_BYTES: Cell<i64> = const { Cell::new(0) };
|
|
178
|
+
/// Routing indexes of finalized documents, free for new documents.
|
|
179
|
+
static FREE_ROUTING_INDEXES: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
|
|
180
|
+
static NEXT_ROUTING_INDEX: Cell<u32> = const { Cell::new(0) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static LIVE_DOCUMENTS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
|
184
|
+
|
|
185
|
+
/// Counts documents whose native storage has not been dropped yet.
|
|
186
|
+
struct LiveDocument;
|
|
187
|
+
|
|
188
|
+
impl LiveDocument {
|
|
189
|
+
fn new() -> Self {
|
|
190
|
+
LIVE_DOCUMENTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
191
|
+
Self
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl Drop for LiveDocument {
|
|
196
|
+
fn drop(&mut self) {
|
|
197
|
+
LIVE_DOCUMENTS.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Trees with more nodes than this are freed on the shared pool, so a
|
|
202
|
+
/// collection pause does not wait for them.
|
|
203
|
+
const POOL_RELEASE_NODES: usize = 4096;
|
|
204
|
+
|
|
92
205
|
impl ObjectFinalize for NativeDocument {
|
|
93
|
-
fn finalize(self,
|
|
206
|
+
fn finalize(self, _env: Env) -> Result<()> {
|
|
207
|
+
// Adjusting external memory during garbage collection is not allowed;
|
|
208
|
+
// the next report on this thread returns it.
|
|
94
209
|
let reported = self.reported_heap_bytes.get();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
210
|
+
RELEASED_HEAP_BYTES.with(|released| released.set(released.get() + reported));
|
|
211
|
+
if let Some(index) = self.routing_index.get() {
|
|
212
|
+
FREE_ROUTING_INDEXES.with(|free| free.borrow_mut().push(index));
|
|
213
|
+
}
|
|
214
|
+
let nodes = self.ids.borrow().len()
|
|
215
|
+
+ self
|
|
216
|
+
.arena
|
|
217
|
+
.borrow()
|
|
218
|
+
.as_ref()
|
|
219
|
+
.map_or(0, |arena| arena.nodes.len());
|
|
220
|
+
if nodes > POOL_RELEASE_NODES {
|
|
221
|
+
runtime::release_later((
|
|
222
|
+
self.html.into_inner(),
|
|
223
|
+
self.arena.into_inner(),
|
|
224
|
+
self.ids.into_inner(),
|
|
225
|
+
self.reverse.into_inner(),
|
|
226
|
+
self.tree_index.into_inner(),
|
|
227
|
+
));
|
|
98
228
|
}
|
|
99
229
|
Ok(())
|
|
100
230
|
}
|
|
@@ -120,16 +250,31 @@ const HEAP_REPORT_STEP: i64 = 64 * 1024;
|
|
|
120
250
|
impl NativeDocument {
|
|
121
251
|
pub(crate) fn from_arena(arena: arena::DomArena, options: Value) -> Self {
|
|
122
252
|
let content_bytes = arena.nodes.iter().map(arena::DomNode::content_bytes).sum();
|
|
253
|
+
Self::from_arena_sized(arena, options, content_bytes)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// A document over `arena` whose text and attribute content takes about
|
|
257
|
+
/// `content_bytes`, such as the length of the source it was parsed from.
|
|
258
|
+
fn from_arena_sized(arena: arena::DomArena, options: Value, content_bytes: usize) -> Self {
|
|
259
|
+
if arena.nodes.len() > POOL_RELEASE_NODES {
|
|
260
|
+
runtime::start_release_thread();
|
|
261
|
+
}
|
|
123
262
|
Self {
|
|
124
263
|
html: RefCell::new(None),
|
|
125
264
|
ids: RefCell::new(Vec::new()),
|
|
126
|
-
reverse: RefCell::new(
|
|
265
|
+
reverse: RefCell::new(id_hash::IdMap::default()),
|
|
266
|
+
tree_index: RefCell::new(None),
|
|
127
267
|
arena: RefCell::new(Some(arena)),
|
|
128
268
|
options,
|
|
129
269
|
source: RefCell::new(None),
|
|
130
270
|
retained: RefCell::new(HashMap::new()),
|
|
131
271
|
needs_collect: Cell::new(false),
|
|
132
272
|
managed: Cell::new(false),
|
|
273
|
+
kept_slots: Cell::new(0),
|
|
274
|
+
routing_index: Cell::new(None),
|
|
275
|
+
tree_detached: Cell::new(false),
|
|
276
|
+
tree_touched: RefCell::new(Vec::new()),
|
|
277
|
+
_live: LiveDocument::new(),
|
|
133
278
|
diagnostics: RefCell::new(Vec::new()),
|
|
134
279
|
locations: RefCell::new(HashMap::new()),
|
|
135
280
|
content_bytes: Cell::new(content_bytes),
|
|
@@ -168,6 +313,10 @@ impl NativeDocument {
|
|
|
168
313
|
}
|
|
169
314
|
|
|
170
315
|
fn report_heap_bytes(&self, mut env: Env) {
|
|
316
|
+
let released = RELEASED_HEAP_BYTES.with(|released| released.replace(0));
|
|
317
|
+
if released != 0 && env.adjust_external_memory(-released).is_err() {
|
|
318
|
+
RELEASED_HEAP_BYTES.with(|pending| pending.set(pending.get() + released));
|
|
319
|
+
}
|
|
171
320
|
let reported = self.reported_heap_bytes.get();
|
|
172
321
|
let estimated = self.estimated_heap_bytes();
|
|
173
322
|
let change = estimated - reported;
|
|
@@ -196,20 +345,40 @@ impl NativeDocument {
|
|
|
196
345
|
.as_object_mut()
|
|
197
346
|
.and_then(|options| options.remove("__tokenEvents"))
|
|
198
347
|
{
|
|
199
|
-
return Self::
|
|
348
|
+
return Self::from_arena_sized(
|
|
200
349
|
tokenizer::parse(source, &events, parser_options.clone()),
|
|
201
350
|
parser_options,
|
|
351
|
+
source.len(),
|
|
202
352
|
);
|
|
203
353
|
}
|
|
204
354
|
if arena::xml_mode(&parser_options)
|
|
205
355
|
|| parser_options.get("_useHtmlParser2") == Some(&Value::Bool(true))
|
|
206
356
|
{
|
|
207
|
-
return Self::
|
|
357
|
+
return Self::from_arena_sized(
|
|
208
358
|
arena::parse_xml(source, parser_options.clone()),
|
|
209
359
|
parser_options,
|
|
360
|
+
source.len(),
|
|
210
361
|
);
|
|
211
362
|
}
|
|
212
363
|
|
|
364
|
+
let capture = parser_options
|
|
365
|
+
.get("sourceCodeLocationInfo")
|
|
366
|
+
.is_some_and(|value| !matches!(value, Value::Null | Value::Bool(false)))
|
|
367
|
+
|| parser_options.get("__collectParseErrors") == Some(&Value::Bool(true));
|
|
368
|
+
// HTML5 input preprocessing turns CRLF and CR into LF; html5ever does
|
|
369
|
+
// it one character at a time, which dominates pages with CRLF endings.
|
|
370
|
+
// Captured source locations must index the original input instead.
|
|
371
|
+
let normalized;
|
|
372
|
+
let source = if !capture && source.contains('\r') {
|
|
373
|
+
normalized = normalize_newlines(source);
|
|
374
|
+
normalized.as_str()
|
|
375
|
+
} else {
|
|
376
|
+
source
|
|
377
|
+
};
|
|
378
|
+
if !is_document && context_name.is_none() && !capture && is_plain_text(source) {
|
|
379
|
+
return Self::from_html(plain_text_fragment(source), false, parser_options, None);
|
|
380
|
+
}
|
|
381
|
+
|
|
213
382
|
let options = html5ever::driver::ParseOpts {
|
|
214
383
|
tokenizer: html5ever::tokenizer::TokenizerOpts {
|
|
215
384
|
discard_bom: false,
|
|
@@ -224,7 +393,7 @@ impl NativeDocument {
|
|
|
224
393
|
};
|
|
225
394
|
let html = if is_document {
|
|
226
395
|
html5ever::driver::parse_document(
|
|
227
|
-
sink::CompatibleSink::new(
|
|
396
|
+
sink::CompatibleSink::new(sized_html(source, true)),
|
|
228
397
|
options,
|
|
229
398
|
)
|
|
230
399
|
.one(source)
|
|
@@ -232,7 +401,7 @@ impl NativeDocument {
|
|
|
232
401
|
// parse5's default fragment context is a template. A body context
|
|
233
402
|
// discards valid standalone table fragments such as <td> and <tr>.
|
|
234
403
|
html5ever::driver::parse_fragment(
|
|
235
|
-
sink::CompatibleSink::new(
|
|
404
|
+
sink::CompatibleSink::new(sized_html(source, false)),
|
|
236
405
|
options,
|
|
237
406
|
html5ever::QualName::new(
|
|
238
407
|
None,
|
|
@@ -250,10 +419,6 @@ impl NativeDocument {
|
|
|
250
419
|
.one(source)
|
|
251
420
|
};
|
|
252
421
|
|
|
253
|
-
let capture = parser_options
|
|
254
|
-
.get("sourceCodeLocationInfo")
|
|
255
|
-
.is_some_and(|value| !matches!(value, Value::Null | Value::Bool(false)))
|
|
256
|
-
|| parser_options.get("__collectParseErrors") == Some(&Value::Bool(true));
|
|
257
422
|
Self::from_html(
|
|
258
423
|
html,
|
|
259
424
|
is_document,
|
|
@@ -269,18 +434,7 @@ impl NativeDocument {
|
|
|
269
434
|
source: Option<String>,
|
|
270
435
|
) -> Self {
|
|
271
436
|
if !is_document {
|
|
272
|
-
|
|
273
|
-
// Cheerio exposes the fragment's actual children at its root.
|
|
274
|
-
let wrapper = html.tree.root().children().find_map(|node| {
|
|
275
|
-
node.value()
|
|
276
|
-
.as_element()
|
|
277
|
-
.filter(|element| element.name() == "html")
|
|
278
|
-
.map(|_| node.id())
|
|
279
|
-
});
|
|
280
|
-
if let Some(wrapper) = wrapper {
|
|
281
|
-
html.tree.root_mut().reparent_from_id_append(wrapper);
|
|
282
|
-
html.tree.get_mut(wrapper).unwrap().detach();
|
|
283
|
-
}
|
|
437
|
+
unwrap_fragment_root(&mut html);
|
|
284
438
|
}
|
|
285
439
|
|
|
286
440
|
let mut locations = HashMap::new();
|
|
@@ -304,20 +458,7 @@ impl NativeDocument {
|
|
|
304
458
|
|_, _| {},
|
|
305
459
|
);
|
|
306
460
|
if !is_document {
|
|
307
|
-
|
|
308
|
-
.tree
|
|
309
|
-
.root()
|
|
310
|
-
.children()
|
|
311
|
-
.find(|node| {
|
|
312
|
-
node.value()
|
|
313
|
-
.as_element()
|
|
314
|
-
.is_some_and(|element| element.name() == "html")
|
|
315
|
-
})
|
|
316
|
-
.map(|node| node.id());
|
|
317
|
-
if let Some(wrapper) = wrapper {
|
|
318
|
-
tracked.tree.root_mut().reparent_from_id_append(wrapper);
|
|
319
|
-
tracked.tree.get_mut(wrapper).unwrap().detach();
|
|
320
|
-
}
|
|
461
|
+
unwrap_fragment_root(&mut tracked);
|
|
321
462
|
}
|
|
322
463
|
let order: HashMap<_, _> = tracked
|
|
323
464
|
.tree
|
|
@@ -363,16 +504,25 @@ impl NativeDocument {
|
|
|
363
504
|
})
|
|
364
505
|
.and(source);
|
|
365
506
|
let content_bytes = html.tree.values().map(html_node_content_bytes).sum();
|
|
507
|
+
if ids.len() > POOL_RELEASE_NODES {
|
|
508
|
+
runtime::start_release_thread();
|
|
509
|
+
}
|
|
366
510
|
Self {
|
|
367
511
|
html: RefCell::new(Some(html)),
|
|
368
512
|
ids: RefCell::new(ids),
|
|
369
513
|
reverse: RefCell::new(reverse),
|
|
514
|
+
tree_index: RefCell::new(None),
|
|
370
515
|
arena: RefCell::new(None),
|
|
371
516
|
options: parser_options,
|
|
372
517
|
source: RefCell::new(source),
|
|
373
518
|
retained: RefCell::new(HashMap::new()),
|
|
374
519
|
needs_collect: Cell::new(false),
|
|
375
520
|
managed: Cell::new(false),
|
|
521
|
+
kept_slots: Cell::new(0),
|
|
522
|
+
routing_index: Cell::new(None),
|
|
523
|
+
tree_detached: Cell::new(false),
|
|
524
|
+
tree_touched: RefCell::new(Vec::new()),
|
|
525
|
+
_live: LiveDocument::new(),
|
|
376
526
|
diagnostics: RefCell::new(diagnostics),
|
|
377
527
|
locations: RefCell::new(locations),
|
|
378
528
|
content_bytes: Cell::new(content_bytes),
|
|
@@ -380,13 +530,33 @@ impl NativeDocument {
|
|
|
380
530
|
}
|
|
381
531
|
}
|
|
382
532
|
|
|
533
|
+
/// The arena borrowed for writing. It drops the name index first: a write
|
|
534
|
+
/// may add, remove or reorder elements.
|
|
535
|
+
pub(crate) fn arena_for_write(&self) -> std::cell::RefMut<'_, Option<arena::DomArena>> {
|
|
536
|
+
let mut arena = self.arena.borrow_mut();
|
|
537
|
+
if let Some(arena) = arena.as_mut() {
|
|
538
|
+
arena.name_index = None;
|
|
539
|
+
}
|
|
540
|
+
arena
|
|
541
|
+
}
|
|
542
|
+
|
|
383
543
|
pub(crate) fn ensure_arena(&self) -> Result<()> {
|
|
384
544
|
let missing = self.arena.borrow().is_none();
|
|
385
545
|
if missing {
|
|
386
|
-
let records
|
|
387
|
-
|
|
388
|
-
.
|
|
389
|
-
|
|
546
|
+
let records = {
|
|
547
|
+
let html = self.html.borrow();
|
|
548
|
+
let ids = self.ids.borrow();
|
|
549
|
+
let reverse = self.reverse.borrow();
|
|
550
|
+
let tree = SharedTree {
|
|
551
|
+
html: html
|
|
552
|
+
.as_ref()
|
|
553
|
+
.ok_or_else(|| Error::new(Status::GenericFailure, "Missing parse tree"))?,
|
|
554
|
+
ids: &ids,
|
|
555
|
+
reverse: &reverse,
|
|
556
|
+
};
|
|
557
|
+
runtime::chunked_map(ids.len(), CONVERSION_CHUNK, |range| tree.convert(range))
|
|
558
|
+
};
|
|
559
|
+
let mut arena = arena::DomArena::from_linked_records(records, self.options.clone());
|
|
390
560
|
if let Some(source) = self.source.borrow().as_ref() {
|
|
391
561
|
location::annotate(
|
|
392
562
|
source,
|
|
@@ -412,10 +582,17 @@ impl NativeDocument {
|
|
|
412
582
|
}
|
|
413
583
|
}
|
|
414
584
|
*self.arena.borrow_mut() = Some(arena);
|
|
415
|
-
self.html.borrow_mut().take();
|
|
585
|
+
let html = self.html.borrow_mut().take();
|
|
586
|
+
let reverse = std::mem::take(&mut *self.reverse.borrow_mut());
|
|
587
|
+
let ids = std::mem::take(&mut *self.ids.borrow_mut());
|
|
588
|
+
// Freeing a large parse tree takes as long as copying it; the
|
|
589
|
+
// JavaScript thread need not wait for it. Waking a worker costs
|
|
590
|
+
// more than freeing a small one.
|
|
591
|
+
if ids.len() > POOL_RELEASE_NODES {
|
|
592
|
+
runtime::release_later((html, reverse, ids));
|
|
593
|
+
}
|
|
416
594
|
self.source.borrow_mut().take();
|
|
417
|
-
*self.
|
|
418
|
-
*self.reverse.borrow_mut() = HashMap::new();
|
|
595
|
+
*self.tree_index.borrow_mut() = None;
|
|
419
596
|
}
|
|
420
597
|
Ok(())
|
|
421
598
|
}
|
|
@@ -431,127 +608,246 @@ impl NativeDocument {
|
|
|
431
608
|
Some(operation(node, html))
|
|
432
609
|
}
|
|
433
610
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
611
|
+
/// Runs `operation` over the parse tree, or returns `None` once the
|
|
612
|
+
/// document has moved to the mutable arena.
|
|
613
|
+
fn with_tree_dom<T>(
|
|
614
|
+
&self,
|
|
615
|
+
options: &Value,
|
|
616
|
+
operation: impl FnOnce(&immutable::TreeDom<'_>) -> T,
|
|
617
|
+
) -> Option<T> {
|
|
618
|
+
if self.arena.borrow().is_some() {
|
|
619
|
+
return None;
|
|
620
|
+
}
|
|
621
|
+
let html = self.html.borrow();
|
|
622
|
+
let html = html.as_ref()?;
|
|
623
|
+
let ids = self.ids.borrow();
|
|
624
|
+
if self.tree_index.borrow().is_none() {
|
|
625
|
+
let index = if self.tree_detached.get() {
|
|
626
|
+
immutable::TreeIndex::build_mapped(html, &ids, &self.reverse.borrow())
|
|
627
|
+
} else {
|
|
628
|
+
immutable::TreeIndex::build(html, ids.len())
|
|
438
629
|
};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
630
|
+
*self.tree_index.borrow_mut() = Some(index);
|
|
631
|
+
}
|
|
632
|
+
let index = self.tree_index.borrow();
|
|
633
|
+
let dom = immutable::TreeDom {
|
|
634
|
+
html,
|
|
635
|
+
ids: &ids,
|
|
636
|
+
index: index.as_ref()?,
|
|
637
|
+
options,
|
|
638
|
+
};
|
|
639
|
+
Some(operation(&dom))
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
fn immutable_dom_node(&self, id: u32) -> Option<arena::DomNode> {
|
|
643
|
+
let html = self.html.borrow();
|
|
644
|
+
let ids = self.ids.borrow();
|
|
645
|
+
let reverse = self.reverse.borrow();
|
|
646
|
+
convert_node(html.as_ref()?, &ids, &reverse, id)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn immutable_record(&self, id: u32) -> Option<Value> {
|
|
650
|
+
serde_json::to_value(self.immutable_dom_node(id)?).ok()
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/// Read commands answered from the parse tree; `None` when the command
|
|
654
|
+
/// needs the mutable arena.
|
|
655
|
+
fn tree_read(
|
|
656
|
+
&self,
|
|
657
|
+
command: &str,
|
|
658
|
+
args: &Value,
|
|
659
|
+
ids: &[u32],
|
|
660
|
+
options: &Value,
|
|
661
|
+
) -> Option<Result<Value>> {
|
|
662
|
+
match command {
|
|
663
|
+
"select" | "matches" => {
|
|
664
|
+
let mut options = options.clone();
|
|
665
|
+
if let Some(matches) = args.get("pseudoMatches") {
|
|
666
|
+
options["__pseudoMatches"] = matches.clone();
|
|
474
667
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
.extras
|
|
483
|
-
.insert("x-publicId".into(), Value::String(data.public_id().into()));
|
|
484
|
-
record
|
|
485
|
-
.extras
|
|
486
|
-
.insert("x-systemId".into(), Value::String(data.system_id().into()));
|
|
487
|
-
let mut content = format!("!DOCTYPE {}", data.name());
|
|
488
|
-
if !data.public_id().is_empty() {
|
|
489
|
-
content.push_str(" PUBLIC ");
|
|
490
|
-
content.push_str("e_doctype(data.public_id()));
|
|
491
|
-
} else if !data.system_id().is_empty() {
|
|
492
|
-
content.push_str(" SYSTEM");
|
|
493
|
-
}
|
|
494
|
-
if !data.system_id().is_empty() {
|
|
495
|
-
content.push(' ');
|
|
496
|
-
content.push_str("e_doctype(data.system_id()));
|
|
668
|
+
let selector = args["selector"].as_str().unwrap_or("");
|
|
669
|
+
let contexts = args.get("contexts").map(arena::value_ids);
|
|
670
|
+
self.with_tree_dom(&options, |dom| {
|
|
671
|
+
if command == "select" {
|
|
672
|
+
selector::select(dom, selector, contexts.as_deref())
|
|
673
|
+
} else {
|
|
674
|
+
selector::filter(dom, selector, ids)
|
|
497
675
|
}
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
676
|
+
.map(|ids| serde_json::json!(ids))
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
"uniqueSort" => {
|
|
680
|
+
// Removed nodes have no place in the parse tree's order, so
|
|
681
|
+
// the arena orders them.
|
|
682
|
+
if self.tree_detached.get() {
|
|
683
|
+
return None;
|
|
504
684
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"style" => "style",
|
|
509
|
-
_ => "tag",
|
|
685
|
+
self.with_tree_dom(options, |dom| {
|
|
686
|
+
if !document_order::is_document_ordered(dom, ids) {
|
|
687
|
+
return Ok(serde_json::json!(document_order::unique_sort(dom, ids)));
|
|
510
688
|
}
|
|
511
|
-
.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
689
|
+
// Local ids of a parse tree are preorder positions: document order.
|
|
690
|
+
let mut unique = ids.to_vec();
|
|
691
|
+
unique.sort_unstable();
|
|
692
|
+
unique.dedup();
|
|
693
|
+
Ok(serde_json::json!(unique))
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
"hasClass" => {
|
|
697
|
+
let needle = args["name"].as_str().unwrap_or("");
|
|
698
|
+
self.with_tree_dom(options, |dom| {
|
|
699
|
+
Ok(Value::Bool(ids.iter().any(|id| {
|
|
700
|
+
dom.is_element(*id)
|
|
701
|
+
&& dom
|
|
702
|
+
.element_attr(*id, "class")
|
|
703
|
+
.is_some_and(|classes| immutable::has_class_token(classes, needle))
|
|
704
|
+
})))
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
"contains" => {
|
|
708
|
+
let container = args
|
|
709
|
+
.get("container")
|
|
710
|
+
.or_else(|| args.get("parent"))
|
|
711
|
+
.and_then(Value::as_u64)
|
|
712
|
+
.unwrap_or(0) as u32;
|
|
713
|
+
let contained = args
|
|
714
|
+
.get("contained")
|
|
715
|
+
.or_else(|| args.get("child"))
|
|
716
|
+
.and_then(Value::as_u64)? as u32;
|
|
717
|
+
self.with_tree_dom(options, |dom| {
|
|
718
|
+
Ok(Value::Bool(dom.index.contains(container, contained)))
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
"extractPlan" => Some(extraction::execute_in(self, &args["plan"], ids)),
|
|
722
|
+
_ => None,
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/// A node with only the kind, name and attributes the attribute commands read.
|
|
727
|
+
fn attribute_node(&self, id: u32) -> Option<arena::DomNode> {
|
|
728
|
+
self.with_node(id, |node, _| {
|
|
729
|
+
let mut record = arena::DomNode::new("text");
|
|
730
|
+
if let Node::Element(element) = node.value() {
|
|
731
|
+
record.kind = match element.name() {
|
|
732
|
+
"script" => "script",
|
|
733
|
+
"style" => "style",
|
|
734
|
+
_ => "tag",
|
|
735
|
+
}
|
|
736
|
+
.into();
|
|
737
|
+
record.name = Some(element.name().into());
|
|
738
|
+
record.attribs = Map::with_capacity(element.attrs.len());
|
|
739
|
+
for (name, value) in &element.attrs {
|
|
538
740
|
record
|
|
539
|
-
.
|
|
540
|
-
.insert(
|
|
741
|
+
.attribs
|
|
742
|
+
.insert(name.local.to_string(), Value::String(value.to_string()));
|
|
541
743
|
}
|
|
542
744
|
}
|
|
543
745
|
record
|
|
544
746
|
})
|
|
545
747
|
}
|
|
546
748
|
|
|
547
|
-
|
|
548
|
-
|
|
749
|
+
/// Applies a write that only changes attributes to the parse tree, so the
|
|
750
|
+
/// document is not copied into the mutable arena. The arena's own attribute
|
|
751
|
+
/// logic runs on a temporary arena holding just the target elements.
|
|
752
|
+
fn tree_attribute_write(
|
|
753
|
+
&self,
|
|
754
|
+
command: &str,
|
|
755
|
+
args: &Value,
|
|
756
|
+
ids: &[u32],
|
|
757
|
+
options: &Value,
|
|
758
|
+
) -> Option<Result<Value>> {
|
|
759
|
+
let is_write = match command {
|
|
760
|
+
"attr" | "css" => args.get("value").is_some(),
|
|
761
|
+
"removeAttr" | "addClass" | "removeClass" | "toggleClass" => true,
|
|
762
|
+
_ => false,
|
|
763
|
+
};
|
|
764
|
+
if !is_write || self.source.borrow().is_some() {
|
|
765
|
+
return None;
|
|
766
|
+
}
|
|
767
|
+
let mut targets: Vec<u32> = Vec::new();
|
|
768
|
+
for id in ids {
|
|
769
|
+
if !targets.contains(id) {
|
|
770
|
+
targets.push(*id);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
let nodes: Vec<arena::DomNode> = targets
|
|
774
|
+
.iter()
|
|
775
|
+
.map(|id| self.attribute_node(*id))
|
|
776
|
+
.collect::<Option<_>>()?;
|
|
777
|
+
let before: Vec<Map<String, Value>> =
|
|
778
|
+
nodes.iter().map(|node| node.attribs.clone()).collect();
|
|
779
|
+
let mut scratch = arena::DomArena::from_records(nodes, options.clone());
|
|
780
|
+
if self.options.get("__utf16Codec") == Some(&Value::Bool(true)) {
|
|
781
|
+
scratch.options["__utf16Codec"] = Value::Bool(true);
|
|
782
|
+
}
|
|
783
|
+
let mut local_args = args.clone();
|
|
784
|
+
local_args["ids"] = serde_json::json!(ids
|
|
785
|
+
.iter()
|
|
786
|
+
.map(|id| targets.iter().position(|target| target == id).unwrap_or(0))
|
|
787
|
+
.collect::<Vec<_>>());
|
|
788
|
+
let result = match attributes::execute(&mut scratch, command, &local_args) {
|
|
789
|
+
Ok(Some(result)) => result,
|
|
790
|
+
Ok(None) => return None,
|
|
791
|
+
Err(error) => return Some(Err(error)),
|
|
792
|
+
};
|
|
793
|
+
let mut html = self.html.borrow_mut();
|
|
794
|
+
let html = html.as_mut()?;
|
|
795
|
+
let node_ids = self.ids.borrow();
|
|
796
|
+
for (index, id) in targets.iter().enumerate() {
|
|
797
|
+
let after = &scratch.nodes[index].attribs;
|
|
798
|
+
if *after == before[index] {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
let mut node = html.tree.get_mut(*node_ids.get(*id as usize)?)?;
|
|
802
|
+
let Node::Element(element) = node.value() else {
|
|
803
|
+
continue;
|
|
804
|
+
};
|
|
805
|
+
let attributes = after
|
|
806
|
+
.iter()
|
|
807
|
+
.map(|(name, value)| {
|
|
808
|
+
let qualified = element
|
|
809
|
+
.attrs
|
|
810
|
+
.keys()
|
|
811
|
+
.rev()
|
|
812
|
+
.find(|key| key.local.as_ref() == name)
|
|
813
|
+
.cloned()
|
|
814
|
+
.unwrap_or_else(|| {
|
|
815
|
+
html5ever::QualName::new(
|
|
816
|
+
None,
|
|
817
|
+
html5ever::ns!(),
|
|
818
|
+
html5ever::LocalName::from(name.as_str()),
|
|
819
|
+
)
|
|
820
|
+
});
|
|
821
|
+
let value = value
|
|
822
|
+
.as_str()
|
|
823
|
+
.map_or_else(|| value.to_string(), str::to_owned);
|
|
824
|
+
html5ever::Attribute {
|
|
825
|
+
name: qualified,
|
|
826
|
+
value: value.into(),
|
|
827
|
+
}
|
|
828
|
+
})
|
|
829
|
+
.collect();
|
|
830
|
+
*element = scraper::node::Element::new(element.name.clone(), attributes);
|
|
831
|
+
}
|
|
832
|
+
Some(Ok(result))
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/// Cheerio-compatible selection without copying an unchanged document.
|
|
836
|
+
fn select_native(&self, source: &str, contexts: Option<&[u32]>) -> Result<Vec<u32>> {
|
|
837
|
+
if let Some(result) =
|
|
838
|
+
self.with_tree_dom(&self.options, |dom| selector::select(dom, source, contexts))
|
|
839
|
+
{
|
|
840
|
+
return result;
|
|
841
|
+
}
|
|
842
|
+
self.ensure_arena()?;
|
|
843
|
+
selector::select(self.arena.borrow().as_ref().unwrap(), source, contexts)
|
|
549
844
|
}
|
|
550
845
|
|
|
551
846
|
fn select_ids(&self, source: &str, contexts: Option<&[u32]>) -> Result<Vec<u32>> {
|
|
552
847
|
self.maybe_collect();
|
|
553
848
|
let contexts = contexts.filter(|contexts| *contexts != [0]);
|
|
554
849
|
if self.arena.borrow().is_some()
|
|
850
|
+
|| self.tree_detached.get()
|
|
555
851
|
|| contexts.is_some()
|
|
556
852
|
|| self.options.get("__losslessUnicode") == Some(&Value::Bool(true))
|
|
557
853
|
|| codec::has_encoding(source)
|
|
@@ -564,17 +860,12 @@ impl NativeDocument {
|
|
|
564
860
|
.next()
|
|
565
861
|
.is_some_and(|ch| matches!(ch, '>' | '+' | '~'))
|
|
566
862
|
|| source.contains("!=")
|
|
863
|
+
|| compares_case_insensitive_attribute(source)
|
|
567
864
|
{
|
|
568
|
-
self.
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
let selector = match Selector::parse(&normalized) {
|
|
573
|
-
Ok(selector) => selector,
|
|
574
|
-
Err(_) => {
|
|
575
|
-
self.ensure_arena()?;
|
|
576
|
-
return selector::select(self.arena.borrow().as_ref().unwrap(), source, contexts);
|
|
577
|
-
}
|
|
865
|
+
return self.select_native(source, contexts);
|
|
866
|
+
}
|
|
867
|
+
let Some(selector) = parsed_selector(source) else {
|
|
868
|
+
return self.select_native(source, contexts);
|
|
578
869
|
};
|
|
579
870
|
let mut matched = Vec::new();
|
|
580
871
|
|
|
@@ -686,6 +977,207 @@ impl NativeDocument {
|
|
|
686
977
|
}
|
|
687
978
|
}
|
|
688
979
|
}
|
|
980
|
+
/// Every live node that `root` does not reach; a parse tree has none.
|
|
981
|
+
#[napi(js_name = "detachedSlots")]
|
|
982
|
+
pub fn detached_slots(&self, root: u32) -> Uint32Array {
|
|
983
|
+
if let Some(arena) = self.arena.borrow().as_ref() {
|
|
984
|
+
return Uint32Array::from(arena.detached_slots(root));
|
|
985
|
+
}
|
|
986
|
+
Uint32Array::from(self.tree_detached_slots(root))
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/// Nodes removed from the parse tree itself, which keeps them in place
|
|
990
|
+
/// but unreachable from `root`.
|
|
991
|
+
fn tree_detached_slots(&self, root: u32) -> Vec<u32> {
|
|
992
|
+
if !self.tree_detached.get() {
|
|
993
|
+
return Vec::new();
|
|
994
|
+
}
|
|
995
|
+
let html = self.html.borrow();
|
|
996
|
+
let ids = self.ids.borrow();
|
|
997
|
+
let reverse = self.reverse.borrow();
|
|
998
|
+
let Some(root) = html
|
|
999
|
+
.as_ref()
|
|
1000
|
+
.zip(ids.get(root as usize))
|
|
1001
|
+
.and_then(|(html, id)| html.tree.get(*id))
|
|
1002
|
+
else {
|
|
1003
|
+
return Vec::new();
|
|
1004
|
+
};
|
|
1005
|
+
let mut attached = vec![false; ids.len()];
|
|
1006
|
+
for node in root.descendants() {
|
|
1007
|
+
if let Some(local) = reverse.get(&node.id()) {
|
|
1008
|
+
attached[*local as usize] = true;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
(0..ids.len() as u32)
|
|
1012
|
+
.filter(|local| !attached[*local as usize])
|
|
1013
|
+
.collect()
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/// Sets an attribute on elements, or removes it when `value` is `null`,
|
|
1017
|
+
/// exactly as the `attr` command does, without copying an unchanged
|
|
1018
|
+
/// document into the mutable arena.
|
|
1019
|
+
#[napi(js_name = "setAttribute")]
|
|
1020
|
+
pub fn set_attribute(&self, ids: Vec<u32>, name: String, value: Option<String>) -> Result<()> {
|
|
1021
|
+
if self.arena.borrow().is_none() && self.source.borrow().is_none() {
|
|
1022
|
+
if let Some(html) = self.html.borrow_mut().as_mut() {
|
|
1023
|
+
let node_ids = self.ids.borrow();
|
|
1024
|
+
for id in &ids {
|
|
1025
|
+
let Some(mut node) = node_ids
|
|
1026
|
+
.get(*id as usize)
|
|
1027
|
+
.and_then(|node_id| html.tree.get_mut(*node_id))
|
|
1028
|
+
else {
|
|
1029
|
+
continue;
|
|
1030
|
+
};
|
|
1031
|
+
if let Node::Element(element) = node.value() {
|
|
1032
|
+
let attributes = with_attribute(element, &name, value.as_deref());
|
|
1033
|
+
*element = scraper::node::Element::new(element.name.clone(), attributes);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return Ok(());
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
self.ensure_arena()?;
|
|
1040
|
+
if let Some(arena) = self.arena_for_write().as_mut() {
|
|
1041
|
+
arena.set_attr(&ids, &name, value.as_deref());
|
|
1042
|
+
}
|
|
1043
|
+
Ok(())
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/// This document's routing index, assigned on first call. An index is
|
|
1047
|
+
/// reused only once its document has been finalized, when nothing in
|
|
1048
|
+
/// JavaScript can hold the document's node ids any more.
|
|
1049
|
+
#[napi(js_name = "routingIndex")]
|
|
1050
|
+
pub fn routing_index(&self) -> u32 {
|
|
1051
|
+
if let Some(index) = self.routing_index.get() {
|
|
1052
|
+
return index;
|
|
1053
|
+
}
|
|
1054
|
+
let index = FREE_ROUTING_INDEXES
|
|
1055
|
+
.with(|free| free.borrow_mut().pop())
|
|
1056
|
+
.unwrap_or_else(|| {
|
|
1057
|
+
NEXT_ROUTING_INDEX.with(|next| {
|
|
1058
|
+
let index = next.get();
|
|
1059
|
+
next.set(index.saturating_add(1));
|
|
1060
|
+
index
|
|
1061
|
+
})
|
|
1062
|
+
});
|
|
1063
|
+
self.routing_index.set(Some(index));
|
|
1064
|
+
index
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/// Documents in this process whose native storage has not been dropped,
|
|
1068
|
+
/// for tests of memory release; resident memory is a noisy signal.
|
|
1069
|
+
#[napi(js_name = "liveDocumentCount")]
|
|
1070
|
+
pub fn live_document_count(&self) -> u32 {
|
|
1071
|
+
LIVE_DOCUMENTS.load(std::sync::atomic::Ordering::Relaxed) as u32
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/// Whether removed created nodes and arena growth make a collection worthwhile.
|
|
1075
|
+
#[napi(js_name = "isCollectionDue")]
|
|
1076
|
+
pub fn is_collection_due(&self) -> bool {
|
|
1077
|
+
self.arena
|
|
1078
|
+
.borrow()
|
|
1079
|
+
.as_ref()
|
|
1080
|
+
.is_some_and(arena::DomArena::is_collection_due)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/// The number of node slots, including released ones.
|
|
1084
|
+
#[napi(js_name = "slotCount")]
|
|
1085
|
+
pub fn slot_count(&self) -> u32 {
|
|
1086
|
+
match self.arena.borrow().as_ref() {
|
|
1087
|
+
Some(arena) => arena.nodes.len() as u32,
|
|
1088
|
+
None => self.ids.borrow().len() as u32,
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/// Parents whose child lists changed since the last call; `null` when
|
|
1093
|
+
/// too many changed to list and every parent must be treated as changed.
|
|
1094
|
+
#[napi(js_name = "takeTouched")]
|
|
1095
|
+
pub fn take_touched(&self) -> Option<Uint32Array> {
|
|
1096
|
+
let removed = std::mem::take(&mut *self.tree_touched.borrow_mut());
|
|
1097
|
+
match self.arena_for_write().as_mut() {
|
|
1098
|
+
Some(arena) => arena.take_touched().map(|mut touched| {
|
|
1099
|
+
touched.extend(removed);
|
|
1100
|
+
Uint32Array::from(touched)
|
|
1101
|
+
}),
|
|
1102
|
+
None => Some(Uint32Array::from(removed)),
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/// Removes nodes from the parse tree itself: the common clean-up before
|
|
1107
|
+
/// reading text or markup then needs no copy into the mutable arena.
|
|
1108
|
+
fn remove_from_tree(&self, ids: &[u32]) {
|
|
1109
|
+
let mut html = self.html.borrow_mut();
|
|
1110
|
+
let Some(html) = html.as_mut() else {
|
|
1111
|
+
return;
|
|
1112
|
+
};
|
|
1113
|
+
let node_ids = self.ids.borrow();
|
|
1114
|
+
let reverse = self.reverse.borrow();
|
|
1115
|
+
let mut touched = self.tree_touched.borrow_mut();
|
|
1116
|
+
for id in ids {
|
|
1117
|
+
let Some(node_id) = node_ids.get(*id as usize) else {
|
|
1118
|
+
continue;
|
|
1119
|
+
};
|
|
1120
|
+
let Some(mut node) = html.tree.get_mut(*node_id) else {
|
|
1121
|
+
continue;
|
|
1122
|
+
};
|
|
1123
|
+
let Some(parent) = node.parent().map(|parent| parent.id()) else {
|
|
1124
|
+
continue;
|
|
1125
|
+
};
|
|
1126
|
+
node.detach();
|
|
1127
|
+
touched.extend(reverse.get(&parent).copied());
|
|
1128
|
+
}
|
|
1129
|
+
self.tree_detached.set(true);
|
|
1130
|
+
*self.tree_index.borrow_mut() = None;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/// Replaces every pin with `pins`, the nodes JavaScript still references,
|
|
1134
|
+
/// then reclaims detached nodes that are neither pinned, below
|
|
1135
|
+
/// `keep_below`, nor connected to such a node. Computing pins at
|
|
1136
|
+
/// collection time lets JavaScript track references without a native call
|
|
1137
|
+
/// per node.
|
|
1138
|
+
#[napi(js_name = "collectPinned")]
|
|
1139
|
+
pub fn collect_pinned(
|
|
1140
|
+
&self,
|
|
1141
|
+
env: Env,
|
|
1142
|
+
pins: Uint32Array,
|
|
1143
|
+
keep_below: Option<u32>,
|
|
1144
|
+
) -> PinnedCollection {
|
|
1145
|
+
self.kept_slots.set(keep_below.unwrap_or(0));
|
|
1146
|
+
let mut retained = HashMap::new();
|
|
1147
|
+
{
|
|
1148
|
+
let arena = self.arena.borrow();
|
|
1149
|
+
let immutable_count = self.ids.borrow().len();
|
|
1150
|
+
for id in pins.iter() {
|
|
1151
|
+
let exists = arena
|
|
1152
|
+
.as_ref()
|
|
1153
|
+
.map_or((*id as usize) < immutable_count, |arena| {
|
|
1154
|
+
arena.node(*id).is_some()
|
|
1155
|
+
});
|
|
1156
|
+
if exists {
|
|
1157
|
+
*retained.entry(*id).or_insert(0) += 1;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
*self.retained.borrow_mut() = retained;
|
|
1162
|
+
self.managed.set(true);
|
|
1163
|
+
let stats = self.collect_garbage_and_report(env);
|
|
1164
|
+
let free = self
|
|
1165
|
+
.arena
|
|
1166
|
+
.borrow()
|
|
1167
|
+
.as_ref()
|
|
1168
|
+
.map(|arena| arena.free.clone())
|
|
1169
|
+
.unwrap_or_default();
|
|
1170
|
+
PinnedCollection {
|
|
1171
|
+
live: stats.live,
|
|
1172
|
+
slots: stats.slots,
|
|
1173
|
+
retained: stats.retained,
|
|
1174
|
+
reclaimed: stats.reclaimed,
|
|
1175
|
+
immutable_nodes: stats.immutable_nodes,
|
|
1176
|
+
external_bytes: stats.external_bytes,
|
|
1177
|
+
free: free.into(),
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
689
1181
|
#[napi(js_name = "collectGarbage")]
|
|
690
1182
|
pub fn collect_garbage_and_report(&self, env: Env) -> MemoryStats {
|
|
691
1183
|
let stats = self.collect_garbage();
|
|
@@ -698,13 +1190,17 @@ impl NativeDocument {
|
|
|
698
1190
|
|
|
699
1191
|
pub fn collect_garbage(&self) -> MemoryStats {
|
|
700
1192
|
self.needs_collect.set(false);
|
|
701
|
-
if self.arena.borrow().is_none()
|
|
1193
|
+
if self.arena.borrow().is_none()
|
|
1194
|
+
&& self.managed.get()
|
|
1195
|
+
&& self.kept_slots.get() == 0
|
|
1196
|
+
&& self.retained.borrow().is_empty()
|
|
702
1197
|
{
|
|
703
1198
|
self.html.borrow_mut().take();
|
|
704
1199
|
self.source.borrow_mut().take();
|
|
705
1200
|
let reclaimed = self.ids.borrow().len() as u32;
|
|
706
1201
|
*self.ids.borrow_mut() = Vec::new();
|
|
707
|
-
*self.reverse.borrow_mut() =
|
|
1202
|
+
*self.reverse.borrow_mut() = id_hash::IdMap::default();
|
|
1203
|
+
*self.tree_index.borrow_mut() = None;
|
|
708
1204
|
return MemoryStats {
|
|
709
1205
|
live: 0,
|
|
710
1206
|
slots: 0,
|
|
@@ -714,8 +1210,13 @@ impl NativeDocument {
|
|
|
714
1210
|
external_bytes: self.reported_heap_bytes.get(),
|
|
715
1211
|
};
|
|
716
1212
|
}
|
|
717
|
-
if let Some(arena) = self.
|
|
718
|
-
let stats = gc::collect_managed(
|
|
1213
|
+
if let Some(arena) = self.arena_for_write().as_mut() {
|
|
1214
|
+
let stats = gc::collect_managed(
|
|
1215
|
+
arena,
|
|
1216
|
+
&self.retained.borrow(),
|
|
1217
|
+
!self.managed.get(),
|
|
1218
|
+
self.kept_slots.get(),
|
|
1219
|
+
);
|
|
719
1220
|
arena.free = stats.free;
|
|
720
1221
|
MemoryStats {
|
|
721
1222
|
live: stats.live,
|
|
@@ -806,7 +1307,7 @@ impl NativeDocument {
|
|
|
806
1307
|
if (self.tag_name(id).as_deref() == Some("template") || self.arena.borrow().is_some())
|
|
807
1308
|
&& self.ensure_arena().is_ok()
|
|
808
1309
|
{
|
|
809
|
-
return self.
|
|
1310
|
+
return self.arena_for_write().as_mut().unwrap().inner_html_mut(id);
|
|
810
1311
|
}
|
|
811
1312
|
self.serialize_nodes(&[id], true)
|
|
812
1313
|
}
|
|
@@ -816,6 +1317,53 @@ impl NativeDocument {
|
|
|
816
1317
|
self.serialize_nodes(&ids, false)
|
|
817
1318
|
}
|
|
818
1319
|
|
|
1320
|
+
/// Serializes nodes as the `render` command does, returning the markup
|
|
1321
|
+
/// itself rather than markup encoded as JSON.
|
|
1322
|
+
#[napi(js_name = "render")]
|
|
1323
|
+
pub fn render_and_report(
|
|
1324
|
+
&self,
|
|
1325
|
+
env: Env,
|
|
1326
|
+
ids: Vec<u32>,
|
|
1327
|
+
options_json: Option<String>,
|
|
1328
|
+
) -> Result<String> {
|
|
1329
|
+
self.maybe_collect();
|
|
1330
|
+
let overrides = match options_json {
|
|
1331
|
+
Some(json) => Some(serde_json::from_str::<Value>(&json).map_err(json_error)?),
|
|
1332
|
+
None => None,
|
|
1333
|
+
}
|
|
1334
|
+
.filter(Value::is_object);
|
|
1335
|
+
let rendered = self.render_nodes(&ids, overrides)?;
|
|
1336
|
+
Ok(self.reported(env, rendered))
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
fn render_nodes(&self, ids: &[u32], overrides: Option<Value>) -> Result<String> {
|
|
1340
|
+
if self.arena.borrow().is_none() {
|
|
1341
|
+
let mut options = self.options.clone();
|
|
1342
|
+
if let Some(Value::Object(overrides)) = &overrides {
|
|
1343
|
+
for (key, value) in overrides {
|
|
1344
|
+
options[key] = value.clone();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
let options = arena::normalized_options(options);
|
|
1348
|
+
if !arena::xml_mode(&options)
|
|
1349
|
+
&& options.get("_useHtmlParser2") != Some(&Value::Bool(true))
|
|
1350
|
+
{
|
|
1351
|
+
return Ok(self.serialize_nodes(ids, false));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
self.ensure_arena()?;
|
|
1355
|
+
let arena = self.arena.borrow();
|
|
1356
|
+
let arena = arena.as_ref().unwrap();
|
|
1357
|
+
let mut options = match overrides {
|
|
1358
|
+
Some(overrides) => arena::normalized_options(overrides),
|
|
1359
|
+
None => arena.options.clone(),
|
|
1360
|
+
};
|
|
1361
|
+
if self.options.get("__utf16Codec") == Some(&Value::Bool(true)) {
|
|
1362
|
+
options["__utf16Codec"] = Value::Bool(true);
|
|
1363
|
+
}
|
|
1364
|
+
Ok(arena.serialize(ids, &arena::normalized_options(options)))
|
|
1365
|
+
}
|
|
1366
|
+
|
|
819
1367
|
#[napi]
|
|
820
1368
|
pub fn attr(&self, id: u32, name: String) -> Option<String> {
|
|
821
1369
|
self.attribute(id, &name)
|
|
@@ -852,6 +1400,18 @@ impl NativeDocument {
|
|
|
852
1400
|
serde_json::to_string(&records).map_err(json_error)
|
|
853
1401
|
}
|
|
854
1402
|
|
|
1403
|
+
/// Every node record at once, in the compact form `snapshot.rs` describes.
|
|
1404
|
+
#[napi]
|
|
1405
|
+
pub fn snapshot(&self, env: Env) -> Result<Object> {
|
|
1406
|
+
self.maybe_collect();
|
|
1407
|
+
if self.source.borrow().is_some() {
|
|
1408
|
+
self.ensure_arena()?;
|
|
1409
|
+
}
|
|
1410
|
+
let built = snapshot::build(self);
|
|
1411
|
+
self.report_heap_bytes(env);
|
|
1412
|
+
built.into_js(&env)
|
|
1413
|
+
}
|
|
1414
|
+
|
|
855
1415
|
#[napi(js_name = "node")]
|
|
856
1416
|
pub fn node_and_report(&self, env: Env, id: u32) -> Result<String> {
|
|
857
1417
|
self.reported(env, self.node(id))
|
|
@@ -921,7 +1481,7 @@ impl NativeDocument {
|
|
|
921
1481
|
}
|
|
922
1482
|
self.ensure_arena()?;
|
|
923
1483
|
source.ensure_arena()?;
|
|
924
|
-
let mut source_arena = source.
|
|
1484
|
+
let mut source_arena = source.arena_for_write();
|
|
925
1485
|
let source_arena = source_arena.as_mut().unwrap();
|
|
926
1486
|
// Detach all requested roots first: overlapping roots become siblings
|
|
927
1487
|
// and each requested descendant is imported exactly once.
|
|
@@ -940,7 +1500,7 @@ impl NativeDocument {
|
|
|
940
1500
|
stack.extend(node.children.iter().rev().copied());
|
|
941
1501
|
}
|
|
942
1502
|
}
|
|
943
|
-
let mut target_arena = self.
|
|
1503
|
+
let mut target_arena = self.arena_for_write();
|
|
944
1504
|
let target_arena = target_arena.as_mut().unwrap();
|
|
945
1505
|
target_arena.nodes.reserve(moved.len());
|
|
946
1506
|
let (roots, mapping) = if detach.unwrap_or(true) {
|
|
@@ -1014,6 +1574,14 @@ impl NativeDocument {
|
|
|
1014
1574
|
if command == "node" {
|
|
1015
1575
|
return self.node(args["id"].as_u64().unwrap_or(0) as u32);
|
|
1016
1576
|
}
|
|
1577
|
+
if command == "remove"
|
|
1578
|
+
&& args["keepChildren"].as_bool() != Some(true)
|
|
1579
|
+
&& self.arena.borrow().is_none()
|
|
1580
|
+
&& self.source.borrow().is_none()
|
|
1581
|
+
{
|
|
1582
|
+
self.remove_from_tree(&arena::value_ids(&args["ids"]));
|
|
1583
|
+
return Ok("null".into());
|
|
1584
|
+
}
|
|
1017
1585
|
if self.arena.borrow().is_none() {
|
|
1018
1586
|
let ids = arena::value_ids(&args["ids"]);
|
|
1019
1587
|
let effective_options = arena::normalized_options(
|
|
@@ -1078,6 +1646,14 @@ impl NativeDocument {
|
|
|
1078
1646
|
.unwrap_or(Value::Null);
|
|
1079
1647
|
return serde_json::to_string(&result).map_err(json_error);
|
|
1080
1648
|
}
|
|
1649
|
+
if let Some(result) = self.tree_read(&command, &args, &ids, &effective_options) {
|
|
1650
|
+
return serde_json::to_string(&result?).map_err(json_error);
|
|
1651
|
+
}
|
|
1652
|
+
if let Some(result) =
|
|
1653
|
+
self.tree_attribute_write(&command, &args, &ids, &effective_options)
|
|
1654
|
+
{
|
|
1655
|
+
return serde_json::to_string(&result?).map_err(json_error);
|
|
1656
|
+
}
|
|
1081
1657
|
if command == "render" || command == "outerHtml" {
|
|
1082
1658
|
let mut options = self.options.clone();
|
|
1083
1659
|
if let Some(overrides) = args.get("options").and_then(Value::as_object) {
|
|
@@ -1095,7 +1671,7 @@ impl NativeDocument {
|
|
|
1095
1671
|
}
|
|
1096
1672
|
}
|
|
1097
1673
|
self.ensure_arena()?;
|
|
1098
|
-
let mut arena = self.
|
|
1674
|
+
let mut arena = self.arena_for_write();
|
|
1099
1675
|
let arena = arena.as_mut().unwrap();
|
|
1100
1676
|
let previous_options = arena.options.clone();
|
|
1101
1677
|
if let Some(options) = args.get("options").filter(|options| options.is_object()) {
|
|
@@ -1124,6 +1700,18 @@ impl NativeDocument {
|
|
|
1124
1700
|
}
|
|
1125
1701
|
}
|
|
1126
1702
|
|
|
1703
|
+
#[napi(object)]
|
|
1704
|
+
pub struct PinnedCollection {
|
|
1705
|
+
pub live: u32,
|
|
1706
|
+
pub slots: u32,
|
|
1707
|
+
pub retained: u32,
|
|
1708
|
+
pub reclaimed: u32,
|
|
1709
|
+
pub immutable_nodes: u32,
|
|
1710
|
+
pub external_bytes: i64,
|
|
1711
|
+
/// Slots that hold no node after collection.
|
|
1712
|
+
pub free: Uint32Array,
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1127
1715
|
#[napi(object)]
|
|
1128
1716
|
pub struct MemoryStats {
|
|
1129
1717
|
pub live: u32,
|
|
@@ -1203,6 +1791,309 @@ fn normalize_attribute_namespaces(source: &str) -> Cow<'_, str> {
|
|
|
1203
1791
|
Cow::Owned(normalized)
|
|
1204
1792
|
}
|
|
1205
1793
|
|
|
1794
|
+
/// An element's attributes after setting or removing `name`. Attributes are
|
|
1795
|
+
/// keyed by local name as in the arena: a repeated local name keeps its first
|
|
1796
|
+
/// position, with the name and value of its last occurrence.
|
|
1797
|
+
fn with_attribute(
|
|
1798
|
+
element: &scraper::node::Element,
|
|
1799
|
+
name: &str,
|
|
1800
|
+
value: Option<&str>,
|
|
1801
|
+
) -> Vec<html5ever::Attribute> {
|
|
1802
|
+
let mut attributes: Vec<html5ever::Attribute> = Vec::with_capacity(element.attrs.len() + 1);
|
|
1803
|
+
for (key, current) in &element.attrs {
|
|
1804
|
+
let attribute = html5ever::Attribute {
|
|
1805
|
+
name: key.clone(),
|
|
1806
|
+
value: current.as_ref().into(),
|
|
1807
|
+
};
|
|
1808
|
+
match attributes
|
|
1809
|
+
.iter_mut()
|
|
1810
|
+
.find(|existing| existing.name.local == key.local)
|
|
1811
|
+
{
|
|
1812
|
+
Some(existing) => *existing = attribute,
|
|
1813
|
+
None => attributes.push(attribute),
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
match value {
|
|
1817
|
+
Some(value) => match attributes
|
|
1818
|
+
.iter_mut()
|
|
1819
|
+
.find(|attribute| attribute.name.local.as_ref() == name)
|
|
1820
|
+
{
|
|
1821
|
+
Some(attribute) => attribute.value = value.into(),
|
|
1822
|
+
None => attributes.push(html5ever::Attribute {
|
|
1823
|
+
name: html5ever::QualName::new(
|
|
1824
|
+
None,
|
|
1825
|
+
html5ever::ns!(),
|
|
1826
|
+
html5ever::LocalName::from(name),
|
|
1827
|
+
),
|
|
1828
|
+
value: value.into(),
|
|
1829
|
+
}),
|
|
1830
|
+
},
|
|
1831
|
+
None => attributes.retain(|attribute| attribute.name.local.as_ref() != name),
|
|
1832
|
+
}
|
|
1833
|
+
attributes
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
thread_local! {
|
|
1837
|
+
/// Parse-tree selectors by source, or `None` for sources they cannot express.
|
|
1838
|
+
static SELECTORS: RefCell<HashMap<String, Option<std::rc::Rc<Selector>>>> = RefCell::new(HashMap::new());
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/// A selector for the parse tree, parsed once per source per thread.
|
|
1842
|
+
fn parsed_selector(source: &str) -> Option<std::rc::Rc<Selector>> {
|
|
1843
|
+
if let Some(parsed) = SELECTORS.with(|cache| cache.borrow().get(source).cloned()) {
|
|
1844
|
+
return parsed;
|
|
1845
|
+
}
|
|
1846
|
+
let parsed = Selector::parse(&normalize_attribute_namespaces(source))
|
|
1847
|
+
.ok()
|
|
1848
|
+
.map(std::rc::Rc::new);
|
|
1849
|
+
SELECTORS.with(|cache| {
|
|
1850
|
+
let mut cache = cache.borrow_mut();
|
|
1851
|
+
if cache.len() >= 512 {
|
|
1852
|
+
cache.clear();
|
|
1853
|
+
}
|
|
1854
|
+
cache.insert(source.to_owned(), parsed.clone());
|
|
1855
|
+
});
|
|
1856
|
+
parsed
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/// Whether a selector compares the value of an attribute whose values HTML
|
|
1860
|
+
/// matches without regard to case, which the parse-tree selector engine does not.
|
|
1861
|
+
fn compares_case_insensitive_attribute(source: &str) -> bool {
|
|
1862
|
+
source.contains('=')
|
|
1863
|
+
&& source.match_indices('[').any(|(index, _)| {
|
|
1864
|
+
let name: String = source[index + 1..]
|
|
1865
|
+
.trim_start()
|
|
1866
|
+
.chars()
|
|
1867
|
+
.take_while(|character| character.is_ascii_alphanumeric() || *character == '-')
|
|
1868
|
+
.collect();
|
|
1869
|
+
selector::CASE_INSENSITIVE_ATTRIBUTES
|
|
1870
|
+
.iter()
|
|
1871
|
+
.any(|candidate| name.eq_ignore_ascii_case(candidate))
|
|
1872
|
+
})
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/// `source` with CRLF and CR replaced by LF, as HTML5 input preprocessing does.
|
|
1876
|
+
fn normalize_newlines(source: &str) -> String {
|
|
1877
|
+
let mut output = String::with_capacity(source.len());
|
|
1878
|
+
let mut rest = source;
|
|
1879
|
+
while let Some(index) = rest.find('\r') {
|
|
1880
|
+
output.push_str(&rest[..index]);
|
|
1881
|
+
output.push('\n');
|
|
1882
|
+
rest = &rest[index + 1..];
|
|
1883
|
+
if let Some(stripped) = rest.strip_prefix('\n') {
|
|
1884
|
+
rest = stripped;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
output.push_str(rest);
|
|
1888
|
+
output
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/// Whether HTML5 parses `source` as a fragment to a single text node holding
|
|
1892
|
+
/// it unchanged: no markup, character references, carriage returns to
|
|
1893
|
+
/// normalize or NUL characters to replace.
|
|
1894
|
+
fn is_plain_text(source: &str) -> bool {
|
|
1895
|
+
!source
|
|
1896
|
+
.bytes()
|
|
1897
|
+
.any(|byte| matches!(byte, b'<' | b'&' | b'\r' | b'\0'))
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/// The fragment HTML5 builds from plain text, without running the parser,
|
|
1901
|
+
/// whose fixed setup costs more than parsing a short string.
|
|
1902
|
+
fn plain_text_fragment(source: &str) -> Html {
|
|
1903
|
+
let mut html = Html::new_fragment();
|
|
1904
|
+
if !source.is_empty() {
|
|
1905
|
+
html.tree.root_mut().append(Node::Text(scraper::node::Text {
|
|
1906
|
+
text: source.into(),
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
html
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/// A parse target with node storage sized for `source`: grown one
|
|
1913
|
+
/// reallocation at a time, it copies every node again, which dominates the
|
|
1914
|
+
/// parse of small inputs. Large documents grow from the cap.
|
|
1915
|
+
fn sized_html(source: &str, is_document: bool) -> Html {
|
|
1916
|
+
const MAXIMUM_INITIAL_NODES: usize = 4096;
|
|
1917
|
+
let capacity = (source.len() / 16 + 8).min(MAXIMUM_INITIAL_NODES);
|
|
1918
|
+
let mut html = if is_document {
|
|
1919
|
+
Html::new_document()
|
|
1920
|
+
} else {
|
|
1921
|
+
Html::new_fragment()
|
|
1922
|
+
};
|
|
1923
|
+
let root = if is_document {
|
|
1924
|
+
Node::Document
|
|
1925
|
+
} else {
|
|
1926
|
+
Node::Fragment
|
|
1927
|
+
};
|
|
1928
|
+
html.tree = ego_tree::Tree::with_capacity(root, capacity);
|
|
1929
|
+
html
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/// html5ever's fragment sink creates a synthetic html element; Cheerio
|
|
1933
|
+
/// exposes the fragment's actual children at its root. Children move one at a
|
|
1934
|
+
/// time because `reparent_from_id_append` only re-points the first and last
|
|
1935
|
+
/// moved child at their new parent.
|
|
1936
|
+
fn unwrap_fragment_root(html: &mut Html) {
|
|
1937
|
+
let wrapper = html.tree.root().children().find_map(|node| {
|
|
1938
|
+
node.value()
|
|
1939
|
+
.as_element()
|
|
1940
|
+
.filter(|element| element.name() == "html")
|
|
1941
|
+
.map(|_| node.id())
|
|
1942
|
+
});
|
|
1943
|
+
let Some(wrapper) = wrapper else { return };
|
|
1944
|
+
let children: Vec<NodeId> = html
|
|
1945
|
+
.tree
|
|
1946
|
+
.get(wrapper)
|
|
1947
|
+
.map(|node| node.children().map(|child| child.id()).collect())
|
|
1948
|
+
.unwrap_or_default();
|
|
1949
|
+
let mut root = html.tree.root_mut();
|
|
1950
|
+
for child in children {
|
|
1951
|
+
root.append_id(child);
|
|
1952
|
+
}
|
|
1953
|
+
html.tree.get_mut(wrapper).unwrap().detach();
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/// Nodes converted per worker task when copying a parse tree into the arena.
|
|
1957
|
+
const CONVERSION_CHUNK: usize = 2048;
|
|
1958
|
+
|
|
1959
|
+
/// A parse tree read by several workers at once while copying it into the
|
|
1960
|
+
/// mutable arena.
|
|
1961
|
+
struct SharedTree<'a> {
|
|
1962
|
+
html: &'a Html,
|
|
1963
|
+
ids: &'a [NodeId],
|
|
1964
|
+
reverse: &'a id_hash::IdMap<NodeId, u32>,
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// SAFETY: `Html` is not `Sync` only because elements cache their id and
|
|
1968
|
+
// classes in `OnceCell`s and tendrils keep their header in `Cell`s. Conversion
|
|
1969
|
+
// reads names, attribute lists and text through shared references and never
|
|
1970
|
+
// touches those caches, and the owning thread waits for the workers, so no
|
|
1971
|
+
// write can race these reads.
|
|
1972
|
+
unsafe impl Sync for SharedTree<'_> {}
|
|
1973
|
+
|
|
1974
|
+
impl SharedTree<'_> {
|
|
1975
|
+
fn convert(&self, range: std::ops::Range<usize>) -> Vec<arena::DomNode> {
|
|
1976
|
+
let mut nodes = Vec::with_capacity(range.len());
|
|
1977
|
+
nodes.extend(
|
|
1978
|
+
range.filter_map(|id| convert_node(self.html, self.ids, self.reverse, id as u32)),
|
|
1979
|
+
);
|
|
1980
|
+
nodes
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/// A parse-tree node as a mutable-arena record. It reads only names,
|
|
1985
|
+
/// attributes and text, never the tree's lazily filled caches.
|
|
1986
|
+
fn convert_node(
|
|
1987
|
+
html: &Html,
|
|
1988
|
+
ids: &[NodeId],
|
|
1989
|
+
reverse: &id_hash::IdMap<NodeId, u32>,
|
|
1990
|
+
id: u32,
|
|
1991
|
+
) -> Option<arena::DomNode> {
|
|
1992
|
+
let node = html.tree.get(*ids.get(id as usize)?)?;
|
|
1993
|
+
let pointer =
|
|
1994
|
+
|node: Option<NodeRef<'_, Node>>| node.and_then(|node| reverse.get(&node.id()).copied());
|
|
1995
|
+
let mut record = arena::DomNode::new("root");
|
|
1996
|
+
record.id = id;
|
|
1997
|
+
record.children = node
|
|
1998
|
+
.children()
|
|
1999
|
+
.filter_map(|child| reverse.get(&child.id()).copied())
|
|
2000
|
+
.collect();
|
|
2001
|
+
record.parent = pointer(node.parent());
|
|
2002
|
+
record.prev = pointer(node.prev_sibling());
|
|
2003
|
+
record.next = pointer(node.next_sibling());
|
|
2004
|
+
match node.value() {
|
|
2005
|
+
Node::Document => {
|
|
2006
|
+
record.extras.insert(
|
|
2007
|
+
"x-mode".into(),
|
|
2008
|
+
Value::String(
|
|
2009
|
+
match html.quirks_mode {
|
|
2010
|
+
html5ever::tree_builder::QuirksMode::Quirks => "quirks",
|
|
2011
|
+
html5ever::tree_builder::QuirksMode::LimitedQuirks => "limited-quirks",
|
|
2012
|
+
html5ever::tree_builder::QuirksMode::NoQuirks => "no-quirks",
|
|
2013
|
+
}
|
|
2014
|
+
.into(),
|
|
2015
|
+
),
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
Node::Fragment => {}
|
|
2019
|
+
Node::Text(data) => {
|
|
2020
|
+
record.kind = "text".into();
|
|
2021
|
+
record.data = Some(String::from(&**data));
|
|
2022
|
+
}
|
|
2023
|
+
Node::Comment(data) => {
|
|
2024
|
+
record.kind = "comment".into();
|
|
2025
|
+
record.data = Some(String::from(&**data));
|
|
2026
|
+
}
|
|
2027
|
+
Node::Doctype(data) => {
|
|
2028
|
+
record.kind = "directive".into();
|
|
2029
|
+
record.name = Some("!doctype".into());
|
|
2030
|
+
record
|
|
2031
|
+
.extras
|
|
2032
|
+
.insert("x-name".into(), Value::String(data.name().into()));
|
|
2033
|
+
record
|
|
2034
|
+
.extras
|
|
2035
|
+
.insert("x-publicId".into(), Value::String(data.public_id().into()));
|
|
2036
|
+
record
|
|
2037
|
+
.extras
|
|
2038
|
+
.insert("x-systemId".into(), Value::String(data.system_id().into()));
|
|
2039
|
+
record.data = Some(doctype_content(data));
|
|
2040
|
+
}
|
|
2041
|
+
Node::ProcessingInstruction(data) => {
|
|
2042
|
+
record.kind = "directive".into();
|
|
2043
|
+
record.name = Some(String::from(&*data.target));
|
|
2044
|
+
record.data = Some(String::from(&*data.data));
|
|
2045
|
+
}
|
|
2046
|
+
Node::Element(element) => {
|
|
2047
|
+
record.kind = match element.name() {
|
|
2048
|
+
"script" => "script",
|
|
2049
|
+
"style" => "style",
|
|
2050
|
+
_ => "tag",
|
|
2051
|
+
}
|
|
2052
|
+
.into();
|
|
2053
|
+
record.name = Some(element.name().into());
|
|
2054
|
+
record.namespace = Some(arena::namespace_name(&element.name.ns));
|
|
2055
|
+
if let Some(prefix) = &element.name.prefix {
|
|
2056
|
+
record
|
|
2057
|
+
.extras
|
|
2058
|
+
.insert("prefix".into(), Value::String(String::from(&**prefix)));
|
|
2059
|
+
}
|
|
2060
|
+
let (mut namespaces, mut prefixes) = (Map::new(), Map::new());
|
|
2061
|
+
record.attribs = Map::with_capacity(element.attrs.len());
|
|
2062
|
+
for (name, value) in &element.attrs {
|
|
2063
|
+
let key = String::from(&*name.local);
|
|
2064
|
+
if !name.ns.is_empty() {
|
|
2065
|
+
namespaces.insert(key.clone(), Value::String(String::from(&*name.ns)));
|
|
2066
|
+
}
|
|
2067
|
+
if let Some(prefix) = &name.prefix {
|
|
2068
|
+
prefixes.insert(key.clone(), Value::String(String::from(&**prefix)));
|
|
2069
|
+
}
|
|
2070
|
+
record
|
|
2071
|
+
.attribs
|
|
2072
|
+
.insert(key, Value::String(String::from(&**value)));
|
|
2073
|
+
}
|
|
2074
|
+
record.attribs_namespace = Some(Box::new(namespaces));
|
|
2075
|
+
record.attribs_prefix = Some(Box::new(prefixes));
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
Some(record)
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/// A doctype's `data`, as htmlparser2 reports the declaration.
|
|
2082
|
+
pub(crate) fn doctype_content(data: &scraper::node::Doctype) -> String {
|
|
2083
|
+
let mut content = format!("!DOCTYPE {}", data.name());
|
|
2084
|
+
if !data.public_id().is_empty() {
|
|
2085
|
+
content.push_str(" PUBLIC ");
|
|
2086
|
+
content.push_str("e_doctype(data.public_id()));
|
|
2087
|
+
} else if !data.system_id().is_empty() {
|
|
2088
|
+
content.push_str(" SYSTEM");
|
|
2089
|
+
}
|
|
2090
|
+
if !data.system_id().is_empty() {
|
|
2091
|
+
content.push(' ');
|
|
2092
|
+
content.push_str("e_doctype(data.system_id()));
|
|
2093
|
+
}
|
|
2094
|
+
content
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1206
2097
|
fn quote_doctype(value: &str) -> String {
|
|
1207
2098
|
let quote = if value.contains('"') { '\'' } else { '"' };
|
|
1208
2099
|
format!("{quote}{value}{quote}")
|
|
@@ -1323,7 +2214,13 @@ fn serialize_node(node: NodeRef<'_, Node>, output: &mut String) {
|
|
|
1323
2214
|
Node::Element(element) => {
|
|
1324
2215
|
output.push('<');
|
|
1325
2216
|
output.push_str(element.name());
|
|
1326
|
-
|
|
2217
|
+
let attributes = js_property_order(
|
|
2218
|
+
element
|
|
2219
|
+
.attrs
|
|
2220
|
+
.iter()
|
|
2221
|
+
.map(|(name, value)| (name.local.as_ref(), (name, value))),
|
|
2222
|
+
);
|
|
2223
|
+
for (_, (name, value)) in attributes {
|
|
1327
2224
|
output.push(' ');
|
|
1328
2225
|
push_attribute_name(
|
|
1329
2226
|
output,
|
|
@@ -1589,6 +2486,32 @@ pub fn extract_plan_many_async(
|
|
|
1589
2486
|
mod tests {
|
|
1590
2487
|
use super::*;
|
|
1591
2488
|
|
|
2489
|
+
#[test]
|
|
2490
|
+
fn attribute_writes_keep_the_parse_tree() {
|
|
2491
|
+
let document = NativeDocument::parse(
|
|
2492
|
+
"<main><p id='a' class='x'>one</p><svg><use href='#h' xlink:href='#x'/></svg></main>",
|
|
2493
|
+
false,
|
|
2494
|
+
);
|
|
2495
|
+
let ids = document.select_ids("p, use", None).unwrap();
|
|
2496
|
+
for (command, args) in [
|
|
2497
|
+
(
|
|
2498
|
+
"attr",
|
|
2499
|
+
serde_json::json!({"ids":ids,"name":"data-k","value":"v"}),
|
|
2500
|
+
),
|
|
2501
|
+
("addClass", serde_json::json!({"ids":ids,"name":"y z"})),
|
|
2502
|
+
("removeClass", serde_json::json!({"ids":ids,"name":"x"})),
|
|
2503
|
+
("removeAttr", serde_json::json!({"ids":ids,"name":"id"})),
|
|
2504
|
+
] {
|
|
2505
|
+
document.op(command.into(), args.to_string()).unwrap();
|
|
2506
|
+
}
|
|
2507
|
+
assert!(document.arena.borrow().is_none());
|
|
2508
|
+
assert_eq!(
|
|
2509
|
+
document.serialize_nodes(&ids, false),
|
|
2510
|
+
"<p class=\"y z\" data-k=\"v\">one</p><use xlink:href=\"#x\" data-k=\"v\" class=\"y z\"></use>"
|
|
2511
|
+
);
|
|
2512
|
+
assert_eq!(document.select_ids(".z", None).unwrap(), ids);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1592
2515
|
#[test]
|
|
1593
2516
|
fn managed_detached_payloads_are_reclaimed_and_slots_are_reused() {
|
|
1594
2517
|
let document = NativeDocument::parse("<main></main>", false);
|
|
@@ -1596,8 +2519,8 @@ mod tests {
|
|
|
1596
2519
|
let main = document.select_ids("main", None).unwrap()[0];
|
|
1597
2520
|
document
|
|
1598
2521
|
.op(
|
|
1599
|
-
"
|
|
1600
|
-
serde_json::json!({"ids":[main],"
|
|
2522
|
+
"clone".into(),
|
|
2523
|
+
serde_json::json!({"ids":[main],"deep":false}).to_string(),
|
|
1601
2524
|
)
|
|
1602
2525
|
.unwrap();
|
|
1603
2526
|
assert_eq!(document.collect_garbage().immutable_nodes, 0);
|
|
@@ -2009,9 +2932,11 @@ mod tests {
|
|
|
2009
2932
|
serde_json::json!({"ids":[detached,ul,detached]}).to_string(),
|
|
2010
2933
|
)
|
|
2011
2934
|
.unwrap();
|
|
2935
|
+
// As domutils does: the last of repeated ids is kept, and nodes of
|
|
2936
|
+
// separate trees keep their order.
|
|
2012
2937
|
assert_eq!(
|
|
2013
2938
|
serde_json::from_str::<Vec<u32>>(&sorted).unwrap(),
|
|
2014
|
-
vec![
|
|
2939
|
+
vec![ul, detached]
|
|
2015
2940
|
);
|
|
2016
2941
|
}
|
|
2017
2942
|
|