rusteerio 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +42 -1
- package/Cargo.toml +8 -1
- package/README.md +90 -51
- package/THIRD_PARTY_NOTICES.md +107 -2
- package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
- package/dist/chunk-FPRP6UIX.mjs.map +1 -0
- package/dist/index.cjs +2232 -750
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs +2226 -745
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.d.mts +2 -2
- package/dist/slim.d.ts +2 -2
- package/dist/slim.mjs +1 -1
- package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
- package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/native/src/arena.rs +219 -80
- package/native/src/attributes.rs +11 -5
- package/native/src/document_order.rs +711 -0
- package/native/src/extraction.rs +15 -3
- package/native/src/gc.rs +12 -3
- package/native/src/id_hash.rs +42 -0
- package/native/src/immutable.rs +256 -0
- package/native/src/lib.rs +1039 -171
- package/native/src/runtime.rs +189 -33
- package/native/src/selector.rs +346 -44
- package/native/src/sink.rs +14 -1
- package/native/src/snapshot.rs +509 -0
- package/native/src/stream.rs +2 -2
- package/native/src/tokenizer.rs +29 -12
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
- package/package.json +3 -1
- package/rusteerio.darwin-arm64.node +0 -0
- package/rusteerio.darwin-x64.node +0 -0
- package/rusteerio.linux-arm64-gnu.node +0 -0
- package/rusteerio.linux-arm64-musl.node +0 -0
- package/rusteerio.linux-x64-gnu.node +0 -0
- package/rusteerio.linux-x64-musl.node +0 -0
- package/rusteerio.win32-arm64-msvc.node +0 -0
- package/rusteerio.win32-x64-msvc.node +0 -0
- package/src/attributes.ts +78 -44
- package/src/codec.ts +22 -3
- package/src/extract.ts +86 -2
- package/src/hooks.ts +148 -2
- package/src/index.ts +51 -40
- package/src/match.ts +207 -0
- package/src/native.ts +9 -1
- package/src/nodes.ts +531 -183
- package/src/router.ts +792 -229
- package/src/selection.ts +239 -125
- package/src/snapshot.ts +255 -0
- package/src/transport.ts +51 -42
- package/dist/chunk-DMRD3FGC.mjs.map +0 -1
package/native/src/lib.rs
CHANGED
|
@@ -14,19 +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;
|
|
26
33
|
mod markup_tokenizer;
|
|
27
34
|
mod runtime;
|
|
28
35
|
mod selector;
|
|
29
36
|
mod sink;
|
|
37
|
+
mod snapshot;
|
|
30
38
|
mod stream;
|
|
31
39
|
mod tokenizer;
|
|
32
40
|
mod tracker;
|
|
@@ -125,13 +133,25 @@ pub struct NativeDocument {
|
|
|
125
133
|
// Stable public IDs are preorder positions, rather than parser allocation
|
|
126
134
|
// positions: HTML5 foster parenting can change the latter's ordering.
|
|
127
135
|
ids: RefCell<Vec<NodeId>>,
|
|
128
|
-
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>>,
|
|
129
139
|
pub(crate) arena: RefCell<Option<arena::DomArena>>,
|
|
130
140
|
options: Value,
|
|
131
141
|
source: RefCell<Option<String>>,
|
|
132
142
|
retained: RefCell<HashMap<u32, usize>>,
|
|
133
143
|
needs_collect: Cell<bool>,
|
|
134
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,
|
|
135
155
|
diagnostics: RefCell<Vec<Value>>,
|
|
136
156
|
locations: RefCell<HashMap<u32, Value>>,
|
|
137
157
|
// V8 cannot see native allocations. Reporting them lets its GC collect
|
|
@@ -140,12 +160,71 @@ pub struct NativeDocument {
|
|
|
140
160
|
reported_heap_bytes: Cell<i64>,
|
|
141
161
|
}
|
|
142
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
|
+
|
|
143
205
|
impl ObjectFinalize for NativeDocument {
|
|
144
|
-
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.
|
|
145
209
|
let reported = self.reported_heap_bytes.get();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
));
|
|
149
228
|
}
|
|
150
229
|
Ok(())
|
|
151
230
|
}
|
|
@@ -171,16 +250,31 @@ const HEAP_REPORT_STEP: i64 = 64 * 1024;
|
|
|
171
250
|
impl NativeDocument {
|
|
172
251
|
pub(crate) fn from_arena(arena: arena::DomArena, options: Value) -> Self {
|
|
173
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
|
+
}
|
|
174
262
|
Self {
|
|
175
263
|
html: RefCell::new(None),
|
|
176
264
|
ids: RefCell::new(Vec::new()),
|
|
177
|
-
reverse: RefCell::new(
|
|
265
|
+
reverse: RefCell::new(id_hash::IdMap::default()),
|
|
266
|
+
tree_index: RefCell::new(None),
|
|
178
267
|
arena: RefCell::new(Some(arena)),
|
|
179
268
|
options,
|
|
180
269
|
source: RefCell::new(None),
|
|
181
270
|
retained: RefCell::new(HashMap::new()),
|
|
182
271
|
needs_collect: Cell::new(false),
|
|
183
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(),
|
|
184
278
|
diagnostics: RefCell::new(Vec::new()),
|
|
185
279
|
locations: RefCell::new(HashMap::new()),
|
|
186
280
|
content_bytes: Cell::new(content_bytes),
|
|
@@ -219,6 +313,10 @@ impl NativeDocument {
|
|
|
219
313
|
}
|
|
220
314
|
|
|
221
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
|
+
}
|
|
222
320
|
let reported = self.reported_heap_bytes.get();
|
|
223
321
|
let estimated = self.estimated_heap_bytes();
|
|
224
322
|
let change = estimated - reported;
|
|
@@ -247,20 +345,40 @@ impl NativeDocument {
|
|
|
247
345
|
.as_object_mut()
|
|
248
346
|
.and_then(|options| options.remove("__tokenEvents"))
|
|
249
347
|
{
|
|
250
|
-
return Self::
|
|
348
|
+
return Self::from_arena_sized(
|
|
251
349
|
tokenizer::parse(source, &events, parser_options.clone()),
|
|
252
350
|
parser_options,
|
|
351
|
+
source.len(),
|
|
253
352
|
);
|
|
254
353
|
}
|
|
255
354
|
if arena::xml_mode(&parser_options)
|
|
256
355
|
|| parser_options.get("_useHtmlParser2") == Some(&Value::Bool(true))
|
|
257
356
|
{
|
|
258
|
-
return Self::
|
|
357
|
+
return Self::from_arena_sized(
|
|
259
358
|
arena::parse_xml(source, parser_options.clone()),
|
|
260
359
|
parser_options,
|
|
360
|
+
source.len(),
|
|
261
361
|
);
|
|
262
362
|
}
|
|
263
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
|
+
|
|
264
382
|
let options = html5ever::driver::ParseOpts {
|
|
265
383
|
tokenizer: html5ever::tokenizer::TokenizerOpts {
|
|
266
384
|
discard_bom: false,
|
|
@@ -275,7 +393,7 @@ impl NativeDocument {
|
|
|
275
393
|
};
|
|
276
394
|
let html = if is_document {
|
|
277
395
|
html5ever::driver::parse_document(
|
|
278
|
-
sink::CompatibleSink::new(
|
|
396
|
+
sink::CompatibleSink::new(sized_html(source, true)),
|
|
279
397
|
options,
|
|
280
398
|
)
|
|
281
399
|
.one(source)
|
|
@@ -283,7 +401,7 @@ impl NativeDocument {
|
|
|
283
401
|
// parse5's default fragment context is a template. A body context
|
|
284
402
|
// discards valid standalone table fragments such as <td> and <tr>.
|
|
285
403
|
html5ever::driver::parse_fragment(
|
|
286
|
-
sink::CompatibleSink::new(
|
|
404
|
+
sink::CompatibleSink::new(sized_html(source, false)),
|
|
287
405
|
options,
|
|
288
406
|
html5ever::QualName::new(
|
|
289
407
|
None,
|
|
@@ -301,10 +419,6 @@ impl NativeDocument {
|
|
|
301
419
|
.one(source)
|
|
302
420
|
};
|
|
303
421
|
|
|
304
|
-
let capture = parser_options
|
|
305
|
-
.get("sourceCodeLocationInfo")
|
|
306
|
-
.is_some_and(|value| !matches!(value, Value::Null | Value::Bool(false)))
|
|
307
|
-
|| parser_options.get("__collectParseErrors") == Some(&Value::Bool(true));
|
|
308
422
|
Self::from_html(
|
|
309
423
|
html,
|
|
310
424
|
is_document,
|
|
@@ -320,18 +434,7 @@ impl NativeDocument {
|
|
|
320
434
|
source: Option<String>,
|
|
321
435
|
) -> Self {
|
|
322
436
|
if !is_document {
|
|
323
|
-
|
|
324
|
-
// Cheerio exposes the fragment's actual children at its root.
|
|
325
|
-
let wrapper = html.tree.root().children().find_map(|node| {
|
|
326
|
-
node.value()
|
|
327
|
-
.as_element()
|
|
328
|
-
.filter(|element| element.name() == "html")
|
|
329
|
-
.map(|_| node.id())
|
|
330
|
-
});
|
|
331
|
-
if let Some(wrapper) = wrapper {
|
|
332
|
-
html.tree.root_mut().reparent_from_id_append(wrapper);
|
|
333
|
-
html.tree.get_mut(wrapper).unwrap().detach();
|
|
334
|
-
}
|
|
437
|
+
unwrap_fragment_root(&mut html);
|
|
335
438
|
}
|
|
336
439
|
|
|
337
440
|
let mut locations = HashMap::new();
|
|
@@ -355,20 +458,7 @@ impl NativeDocument {
|
|
|
355
458
|
|_, _| {},
|
|
356
459
|
);
|
|
357
460
|
if !is_document {
|
|
358
|
-
|
|
359
|
-
.tree
|
|
360
|
-
.root()
|
|
361
|
-
.children()
|
|
362
|
-
.find(|node| {
|
|
363
|
-
node.value()
|
|
364
|
-
.as_element()
|
|
365
|
-
.is_some_and(|element| element.name() == "html")
|
|
366
|
-
})
|
|
367
|
-
.map(|node| node.id());
|
|
368
|
-
if let Some(wrapper) = wrapper {
|
|
369
|
-
tracked.tree.root_mut().reparent_from_id_append(wrapper);
|
|
370
|
-
tracked.tree.get_mut(wrapper).unwrap().detach();
|
|
371
|
-
}
|
|
461
|
+
unwrap_fragment_root(&mut tracked);
|
|
372
462
|
}
|
|
373
463
|
let order: HashMap<_, _> = tracked
|
|
374
464
|
.tree
|
|
@@ -414,16 +504,25 @@ impl NativeDocument {
|
|
|
414
504
|
})
|
|
415
505
|
.and(source);
|
|
416
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
|
+
}
|
|
417
510
|
Self {
|
|
418
511
|
html: RefCell::new(Some(html)),
|
|
419
512
|
ids: RefCell::new(ids),
|
|
420
513
|
reverse: RefCell::new(reverse),
|
|
514
|
+
tree_index: RefCell::new(None),
|
|
421
515
|
arena: RefCell::new(None),
|
|
422
516
|
options: parser_options,
|
|
423
517
|
source: RefCell::new(source),
|
|
424
518
|
retained: RefCell::new(HashMap::new()),
|
|
425
519
|
needs_collect: Cell::new(false),
|
|
426
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(),
|
|
427
526
|
diagnostics: RefCell::new(diagnostics),
|
|
428
527
|
locations: RefCell::new(locations),
|
|
429
528
|
content_bytes: Cell::new(content_bytes),
|
|
@@ -431,13 +530,33 @@ impl NativeDocument {
|
|
|
431
530
|
}
|
|
432
531
|
}
|
|
433
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
|
+
|
|
434
543
|
pub(crate) fn ensure_arena(&self) -> Result<()> {
|
|
435
544
|
let missing = self.arena.borrow().is_none();
|
|
436
545
|
if missing {
|
|
437
|
-
let records
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
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());
|
|
441
560
|
if let Some(source) = self.source.borrow().as_ref() {
|
|
442
561
|
location::annotate(
|
|
443
562
|
source,
|
|
@@ -463,10 +582,17 @@ impl NativeDocument {
|
|
|
463
582
|
}
|
|
464
583
|
}
|
|
465
584
|
*self.arena.borrow_mut() = Some(arena);
|
|
466
|
-
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
|
+
}
|
|
467
594
|
self.source.borrow_mut().take();
|
|
468
|
-
*self.
|
|
469
|
-
*self.reverse.borrow_mut() = HashMap::new();
|
|
595
|
+
*self.tree_index.borrow_mut() = None;
|
|
470
596
|
}
|
|
471
597
|
Ok(())
|
|
472
598
|
}
|
|
@@ -482,127 +608,246 @@ impl NativeDocument {
|
|
|
482
608
|
Some(operation(node, html))
|
|
483
609
|
}
|
|
484
610
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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())
|
|
489
629
|
};
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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();
|
|
525
667
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
.extras
|
|
534
|
-
.insert("x-publicId".into(), Value::String(data.public_id().into()));
|
|
535
|
-
record
|
|
536
|
-
.extras
|
|
537
|
-
.insert("x-systemId".into(), Value::String(data.system_id().into()));
|
|
538
|
-
let mut content = format!("!DOCTYPE {}", data.name());
|
|
539
|
-
if !data.public_id().is_empty() {
|
|
540
|
-
content.push_str(" PUBLIC ");
|
|
541
|
-
content.push_str("e_doctype(data.public_id()));
|
|
542
|
-
} else if !data.system_id().is_empty() {
|
|
543
|
-
content.push_str(" SYSTEM");
|
|
544
|
-
}
|
|
545
|
-
if !data.system_id().is_empty() {
|
|
546
|
-
content.push(' ');
|
|
547
|
-
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)
|
|
548
675
|
}
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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;
|
|
555
684
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"style" => "style",
|
|
560
|
-
_ => "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)));
|
|
561
688
|
}
|
|
562
|
-
.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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 {
|
|
589
740
|
record
|
|
590
|
-
.
|
|
591
|
-
.insert(
|
|
741
|
+
.attribs
|
|
742
|
+
.insert(name.local.to_string(), Value::String(value.to_string()));
|
|
592
743
|
}
|
|
593
744
|
}
|
|
594
745
|
record
|
|
595
746
|
})
|
|
596
747
|
}
|
|
597
748
|
|
|
598
|
-
|
|
599
|
-
|
|
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)
|
|
600
844
|
}
|
|
601
845
|
|
|
602
846
|
fn select_ids(&self, source: &str, contexts: Option<&[u32]>) -> Result<Vec<u32>> {
|
|
603
847
|
self.maybe_collect();
|
|
604
848
|
let contexts = contexts.filter(|contexts| *contexts != [0]);
|
|
605
849
|
if self.arena.borrow().is_some()
|
|
850
|
+
|| self.tree_detached.get()
|
|
606
851
|
|| contexts.is_some()
|
|
607
852
|
|| self.options.get("__losslessUnicode") == Some(&Value::Bool(true))
|
|
608
853
|
|| codec::has_encoding(source)
|
|
@@ -615,17 +860,12 @@ impl NativeDocument {
|
|
|
615
860
|
.next()
|
|
616
861
|
.is_some_and(|ch| matches!(ch, '>' | '+' | '~'))
|
|
617
862
|
|| source.contains("!=")
|
|
863
|
+
|| compares_case_insensitive_attribute(source)
|
|
618
864
|
{
|
|
619
|
-
self.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
let selector = match Selector::parse(&normalized) {
|
|
624
|
-
Ok(selector) => selector,
|
|
625
|
-
Err(_) => {
|
|
626
|
-
self.ensure_arena()?;
|
|
627
|
-
return selector::select(self.arena.borrow().as_ref().unwrap(), source, contexts);
|
|
628
|
-
}
|
|
865
|
+
return self.select_native(source, contexts);
|
|
866
|
+
}
|
|
867
|
+
let Some(selector) = parsed_selector(source) else {
|
|
868
|
+
return self.select_native(source, contexts);
|
|
629
869
|
};
|
|
630
870
|
let mut matched = Vec::new();
|
|
631
871
|
|
|
@@ -737,6 +977,207 @@ impl NativeDocument {
|
|
|
737
977
|
}
|
|
738
978
|
}
|
|
739
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
|
+
|
|
740
1181
|
#[napi(js_name = "collectGarbage")]
|
|
741
1182
|
pub fn collect_garbage_and_report(&self, env: Env) -> MemoryStats {
|
|
742
1183
|
let stats = self.collect_garbage();
|
|
@@ -749,13 +1190,17 @@ impl NativeDocument {
|
|
|
749
1190
|
|
|
750
1191
|
pub fn collect_garbage(&self) -> MemoryStats {
|
|
751
1192
|
self.needs_collect.set(false);
|
|
752
|
-
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()
|
|
753
1197
|
{
|
|
754
1198
|
self.html.borrow_mut().take();
|
|
755
1199
|
self.source.borrow_mut().take();
|
|
756
1200
|
let reclaimed = self.ids.borrow().len() as u32;
|
|
757
1201
|
*self.ids.borrow_mut() = Vec::new();
|
|
758
|
-
*self.reverse.borrow_mut() =
|
|
1202
|
+
*self.reverse.borrow_mut() = id_hash::IdMap::default();
|
|
1203
|
+
*self.tree_index.borrow_mut() = None;
|
|
759
1204
|
return MemoryStats {
|
|
760
1205
|
live: 0,
|
|
761
1206
|
slots: 0,
|
|
@@ -765,8 +1210,13 @@ impl NativeDocument {
|
|
|
765
1210
|
external_bytes: self.reported_heap_bytes.get(),
|
|
766
1211
|
};
|
|
767
1212
|
}
|
|
768
|
-
if let Some(arena) = self.
|
|
769
|
-
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
|
+
);
|
|
770
1220
|
arena.free = stats.free;
|
|
771
1221
|
MemoryStats {
|
|
772
1222
|
live: stats.live,
|
|
@@ -857,7 +1307,7 @@ impl NativeDocument {
|
|
|
857
1307
|
if (self.tag_name(id).as_deref() == Some("template") || self.arena.borrow().is_some())
|
|
858
1308
|
&& self.ensure_arena().is_ok()
|
|
859
1309
|
{
|
|
860
|
-
return self.
|
|
1310
|
+
return self.arena_for_write().as_mut().unwrap().inner_html_mut(id);
|
|
861
1311
|
}
|
|
862
1312
|
self.serialize_nodes(&[id], true)
|
|
863
1313
|
}
|
|
@@ -867,6 +1317,53 @@ impl NativeDocument {
|
|
|
867
1317
|
self.serialize_nodes(&ids, false)
|
|
868
1318
|
}
|
|
869
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
|
+
|
|
870
1367
|
#[napi]
|
|
871
1368
|
pub fn attr(&self, id: u32, name: String) -> Option<String> {
|
|
872
1369
|
self.attribute(id, &name)
|
|
@@ -903,6 +1400,18 @@ impl NativeDocument {
|
|
|
903
1400
|
serde_json::to_string(&records).map_err(json_error)
|
|
904
1401
|
}
|
|
905
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
|
+
|
|
906
1415
|
#[napi(js_name = "node")]
|
|
907
1416
|
pub fn node_and_report(&self, env: Env, id: u32) -> Result<String> {
|
|
908
1417
|
self.reported(env, self.node(id))
|
|
@@ -972,7 +1481,7 @@ impl NativeDocument {
|
|
|
972
1481
|
}
|
|
973
1482
|
self.ensure_arena()?;
|
|
974
1483
|
source.ensure_arena()?;
|
|
975
|
-
let mut source_arena = source.
|
|
1484
|
+
let mut source_arena = source.arena_for_write();
|
|
976
1485
|
let source_arena = source_arena.as_mut().unwrap();
|
|
977
1486
|
// Detach all requested roots first: overlapping roots become siblings
|
|
978
1487
|
// and each requested descendant is imported exactly once.
|
|
@@ -991,7 +1500,7 @@ impl NativeDocument {
|
|
|
991
1500
|
stack.extend(node.children.iter().rev().copied());
|
|
992
1501
|
}
|
|
993
1502
|
}
|
|
994
|
-
let mut target_arena = self.
|
|
1503
|
+
let mut target_arena = self.arena_for_write();
|
|
995
1504
|
let target_arena = target_arena.as_mut().unwrap();
|
|
996
1505
|
target_arena.nodes.reserve(moved.len());
|
|
997
1506
|
let (roots, mapping) = if detach.unwrap_or(true) {
|
|
@@ -1065,6 +1574,14 @@ impl NativeDocument {
|
|
|
1065
1574
|
if command == "node" {
|
|
1066
1575
|
return self.node(args["id"].as_u64().unwrap_or(0) as u32);
|
|
1067
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
|
+
}
|
|
1068
1585
|
if self.arena.borrow().is_none() {
|
|
1069
1586
|
let ids = arena::value_ids(&args["ids"]);
|
|
1070
1587
|
let effective_options = arena::normalized_options(
|
|
@@ -1129,6 +1646,14 @@ impl NativeDocument {
|
|
|
1129
1646
|
.unwrap_or(Value::Null);
|
|
1130
1647
|
return serde_json::to_string(&result).map_err(json_error);
|
|
1131
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
|
+
}
|
|
1132
1657
|
if command == "render" || command == "outerHtml" {
|
|
1133
1658
|
let mut options = self.options.clone();
|
|
1134
1659
|
if let Some(overrides) = args.get("options").and_then(Value::as_object) {
|
|
@@ -1146,7 +1671,7 @@ impl NativeDocument {
|
|
|
1146
1671
|
}
|
|
1147
1672
|
}
|
|
1148
1673
|
self.ensure_arena()?;
|
|
1149
|
-
let mut arena = self.
|
|
1674
|
+
let mut arena = self.arena_for_write();
|
|
1150
1675
|
let arena = arena.as_mut().unwrap();
|
|
1151
1676
|
let previous_options = arena.options.clone();
|
|
1152
1677
|
if let Some(options) = args.get("options").filter(|options| options.is_object()) {
|
|
@@ -1175,6 +1700,18 @@ impl NativeDocument {
|
|
|
1175
1700
|
}
|
|
1176
1701
|
}
|
|
1177
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
|
+
|
|
1178
1715
|
#[napi(object)]
|
|
1179
1716
|
pub struct MemoryStats {
|
|
1180
1717
|
pub live: u32,
|
|
@@ -1254,6 +1791,309 @@ fn normalize_attribute_namespaces(source: &str) -> Cow<'_, str> {
|
|
|
1254
1791
|
Cow::Owned(normalized)
|
|
1255
1792
|
}
|
|
1256
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
|
+
|
|
1257
2097
|
fn quote_doctype(value: &str) -> String {
|
|
1258
2098
|
let quote = if value.contains('"') { '\'' } else { '"' };
|
|
1259
2099
|
format!("{quote}{value}{quote}")
|
|
@@ -1646,6 +2486,32 @@ pub fn extract_plan_many_async(
|
|
|
1646
2486
|
mod tests {
|
|
1647
2487
|
use super::*;
|
|
1648
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
|
+
|
|
1649
2515
|
#[test]
|
|
1650
2516
|
fn managed_detached_payloads_are_reclaimed_and_slots_are_reused() {
|
|
1651
2517
|
let document = NativeDocument::parse("<main></main>", false);
|
|
@@ -1653,8 +2519,8 @@ mod tests {
|
|
|
1653
2519
|
let main = document.select_ids("main", None).unwrap()[0];
|
|
1654
2520
|
document
|
|
1655
2521
|
.op(
|
|
1656
|
-
"
|
|
1657
|
-
serde_json::json!({"ids":[main],"
|
|
2522
|
+
"clone".into(),
|
|
2523
|
+
serde_json::json!({"ids":[main],"deep":false}).to_string(),
|
|
1658
2524
|
)
|
|
1659
2525
|
.unwrap();
|
|
1660
2526
|
assert_eq!(document.collect_garbage().immutable_nodes, 0);
|
|
@@ -2066,9 +2932,11 @@ mod tests {
|
|
|
2066
2932
|
serde_json::json!({"ids":[detached,ul,detached]}).to_string(),
|
|
2067
2933
|
)
|
|
2068
2934
|
.unwrap();
|
|
2935
|
+
// As domutils does: the last of repeated ids is kept, and nodes of
|
|
2936
|
+
// separate trees keep their order.
|
|
2069
2937
|
assert_eq!(
|
|
2070
2938
|
serde_json::from_str::<Vec<u32>>(&sorted).unwrap(),
|
|
2071
|
-
vec![
|
|
2939
|
+
vec![ul, detached]
|
|
2072
2940
|
);
|
|
2073
2941
|
}
|
|
2074
2942
|
|