rusteerio 1.1.0 → 1.1.2
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 +1 -1
- package/Cargo.toml +1 -1
- package/README.md +37 -37
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/{chunk-FPRP6UIX.mjs → chunk-HN63P3U7.mjs} +625 -401
- package/dist/chunk-HN63P3U7.mjs.map +1 -0
- package/dist/index.cjs +624 -400
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs +555 -407
- 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-Cs9UP1Vv.d.mts → types-CJsc0PH-.d.mts} +12 -0
- package/dist/{types-Cs9UP1Vv.d.ts → types-CJsc0PH-.d.ts} +12 -0
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/native/src/arena.rs +163 -7
- package/native/src/attributes.rs +18 -1
- package/native/src/codec.rs +79 -0
- package/native/src/lib.rs +117 -30
- package/native/src/snapshot.rs +144 -89
- package/native/src/tokenizer.rs +34 -20
- package/package.json +1 -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/extract.ts +117 -24
- package/src/index.ts +27 -6
- package/src/match.ts +4 -4
- package/src/native.ts +4 -0
- package/src/nodes.ts +70 -10
- package/src/router.ts +80 -4
- package/src/selection.ts +18 -17
- package/src/snapshot.ts +37 -0
- package/src/transport.ts +1 -1
- package/dist/chunk-FPRP6UIX.mjs.map +0 -1
package/native/src/lib.rs
CHANGED
|
@@ -14,6 +14,8 @@ use napi_derive::napi;
|
|
|
14
14
|
use scraper::{Html, Node, Selector};
|
|
15
15
|
use serde_json::{Map, Value};
|
|
16
16
|
|
|
17
|
+
use codec::JsText;
|
|
18
|
+
|
|
17
19
|
#[cfg(target_os = "linux")]
|
|
18
20
|
#[global_allocator]
|
|
19
21
|
static ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
@@ -158,6 +160,11 @@ pub struct NativeDocument {
|
|
|
158
160
|
// unreachable documents under native memory pressure.
|
|
159
161
|
content_bytes: Cell<usize>,
|
|
160
162
|
reported_heap_bytes: Cell<i64>,
|
|
163
|
+
/// Results of extraction plans run on the parsing thread, in request
|
|
164
|
+
/// order, until JavaScript takes them.
|
|
165
|
+
prepared_plans: RefCell<Option<Vec<Option<String>>>>,
|
|
166
|
+
/// A snapshot built on the parsing thread, until JavaScript takes it.
|
|
167
|
+
prepared_snapshot: RefCell<Option<snapshot::NodeSnapshot>>,
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
/// Node-API calls the finalizers of a module that declares the experimental
|
|
@@ -279,6 +286,8 @@ impl NativeDocument {
|
|
|
279
286
|
locations: RefCell::new(HashMap::new()),
|
|
280
287
|
content_bytes: Cell::new(content_bytes),
|
|
281
288
|
reported_heap_bytes: Cell::new(0),
|
|
289
|
+
prepared_plans: RefCell::new(None),
|
|
290
|
+
prepared_snapshot: RefCell::new(None),
|
|
282
291
|
}
|
|
283
292
|
}
|
|
284
293
|
pub(crate) fn set_diagnostic_records(&self, records: Vec<Value>) {
|
|
@@ -527,6 +536,8 @@ impl NativeDocument {
|
|
|
527
536
|
locations: RefCell::new(locations),
|
|
528
537
|
content_bytes: Cell::new(content_bytes),
|
|
529
538
|
reported_heap_bytes: Cell::new(0),
|
|
539
|
+
prepared_plans: RefCell::new(None),
|
|
540
|
+
prepared_snapshot: RefCell::new(None),
|
|
530
541
|
}
|
|
531
542
|
}
|
|
532
543
|
|
|
@@ -540,6 +551,14 @@ impl NativeDocument {
|
|
|
540
551
|
arena
|
|
541
552
|
}
|
|
542
553
|
|
|
554
|
+
/// The arena borrowed for a write that changes only attributes or data,
|
|
555
|
+
/// which keeps the name index valid.
|
|
556
|
+
pub(crate) fn arena_for_attribute_write(
|
|
557
|
+
&self,
|
|
558
|
+
) -> std::cell::RefMut<'_, Option<arena::DomArena>> {
|
|
559
|
+
self.arena.borrow_mut()
|
|
560
|
+
}
|
|
561
|
+
|
|
543
562
|
pub(crate) fn ensure_arena(&self) -> Result<()> {
|
|
544
563
|
let missing = self.arena.borrow().is_none();
|
|
545
564
|
if missing {
|
|
@@ -735,7 +754,9 @@ impl NativeDocument {
|
|
|
735
754
|
}
|
|
736
755
|
.into();
|
|
737
756
|
record.name = Some(element.name().into());
|
|
738
|
-
|
|
757
|
+
if !element.attrs.is_empty() {
|
|
758
|
+
*record.attribs = Map::with_capacity(element.attrs.len());
|
|
759
|
+
}
|
|
739
760
|
for (name, value) in &element.attrs {
|
|
740
761
|
record
|
|
741
762
|
.attribs
|
|
@@ -775,7 +796,7 @@ impl NativeDocument {
|
|
|
775
796
|
.map(|id| self.attribute_node(*id))
|
|
776
797
|
.collect::<Option<_>>()?;
|
|
777
798
|
let before: Vec<Map<String, Value>> =
|
|
778
|
-
nodes.iter().map(|node| node.attribs.clone()).collect();
|
|
799
|
+
nodes.iter().map(|node| (*node.attribs).clone()).collect();
|
|
779
800
|
let mut scratch = arena::DomArena::from_records(nodes, options.clone());
|
|
780
801
|
if self.options.get("__utf16Codec") == Some(&Value::Bool(true)) {
|
|
781
802
|
scratch.options["__utf16Codec"] = Value::Bool(true);
|
|
@@ -795,7 +816,7 @@ impl NativeDocument {
|
|
|
795
816
|
let node_ids = self.ids.borrow();
|
|
796
817
|
for (index, id) in targets.iter().enumerate() {
|
|
797
818
|
let after = &scratch.nodes[index].attribs;
|
|
798
|
-
if
|
|
819
|
+
if **after == before[index] {
|
|
799
820
|
continue;
|
|
800
821
|
}
|
|
801
822
|
let mut node = html.tree.get_mut(*node_ids.get(*id as usize)?)?;
|
|
@@ -1037,7 +1058,7 @@ impl NativeDocument {
|
|
|
1037
1058
|
}
|
|
1038
1059
|
}
|
|
1039
1060
|
self.ensure_arena()?;
|
|
1040
|
-
if let Some(arena) = self.
|
|
1061
|
+
if let Some(arena) = self.arena_for_attribute_write().as_mut() {
|
|
1041
1062
|
arena.set_attr(&ids, &name, value.as_deref());
|
|
1042
1063
|
}
|
|
1043
1064
|
Ok(())
|
|
@@ -1293,14 +1314,46 @@ impl NativeDocument {
|
|
|
1293
1314
|
.collect()
|
|
1294
1315
|
}
|
|
1295
1316
|
|
|
1296
|
-
|
|
1317
|
+
/// The results of the extraction plans `parseManyAsync` ran on the
|
|
1318
|
+
/// document's root while parsing it, each `null` if it failed; `null`
|
|
1319
|
+
/// after the first call.
|
|
1320
|
+
#[napi(
|
|
1321
|
+
js_name = "takePreparedPlans",
|
|
1322
|
+
ts_return_type = "Array<string | null> | null"
|
|
1323
|
+
)]
|
|
1324
|
+
pub fn take_prepared_plans(&self) -> Option<Vec<Option<JsText>>> {
|
|
1325
|
+
self.prepared_plans.borrow_mut().take().map(|results| {
|
|
1326
|
+
results
|
|
1327
|
+
.into_iter()
|
|
1328
|
+
.map(|result| result.map(JsText))
|
|
1329
|
+
.collect()
|
|
1330
|
+
})
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/// The snapshot `parseManyAsync` built while parsing the document, if it
|
|
1334
|
+
/// did; `null` after the first call, which discards it unless `is_wanted`.
|
|
1335
|
+
/// It describes the parsed tree, so JavaScript discards it on its first write.
|
|
1336
|
+
#[napi(js_name = "takePreparedSnapshot")]
|
|
1337
|
+
pub fn take_prepared_snapshot(&self, env: Env, is_wanted: bool) -> Result<Option<Object>> {
|
|
1338
|
+
let prepared = self.prepared_snapshot.borrow_mut().take();
|
|
1339
|
+
match prepared {
|
|
1340
|
+
Some(snapshot) if is_wanted => snapshot.into_js(&env).map(Some),
|
|
1341
|
+
_ => Ok(None),
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
#[napi(js_name = "text", ts_return_type = "string")]
|
|
1346
|
+
pub fn text_for_js(&self, ids: Vec<u32>) -> JsText {
|
|
1347
|
+
JsText(self.text(ids))
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1297
1350
|
pub fn text(&self, ids: Vec<u32>) -> String {
|
|
1298
1351
|
self.text_ids(&ids)
|
|
1299
1352
|
}
|
|
1300
1353
|
|
|
1301
|
-
#[napi(js_name = "innerHtml")]
|
|
1302
|
-
pub fn inner_html_and_report(&self, env: Env, id: u32) ->
|
|
1303
|
-
self.reported(env, self.inner_html(id))
|
|
1354
|
+
#[napi(js_name = "innerHtml", ts_return_type = "string")]
|
|
1355
|
+
pub fn inner_html_and_report(&self, env: Env, id: u32) -> JsText {
|
|
1356
|
+
JsText(self.reported(env, self.inner_html(id)))
|
|
1304
1357
|
}
|
|
1305
1358
|
|
|
1306
1359
|
pub fn inner_html(&self, id: u32) -> String {
|
|
@@ -1312,20 +1365,24 @@ impl NativeDocument {
|
|
|
1312
1365
|
self.serialize_nodes(&[id], true)
|
|
1313
1366
|
}
|
|
1314
1367
|
|
|
1315
|
-
#[napi]
|
|
1368
|
+
#[napi(js_name = "outerHtml", ts_return_type = "string")]
|
|
1369
|
+
pub fn outer_html_for_js(&self, ids: Vec<u32>) -> JsText {
|
|
1370
|
+
JsText(self.outer_html(ids))
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1316
1373
|
pub fn outer_html(&self, ids: Vec<u32>) -> String {
|
|
1317
1374
|
self.serialize_nodes(&ids, false)
|
|
1318
1375
|
}
|
|
1319
1376
|
|
|
1320
1377
|
/// Serializes nodes as the `render` command does, returning the markup
|
|
1321
1378
|
/// itself rather than markup encoded as JSON.
|
|
1322
|
-
#[napi(js_name = "render")]
|
|
1379
|
+
#[napi(js_name = "render", ts_return_type = "string")]
|
|
1323
1380
|
pub fn render_and_report(
|
|
1324
1381
|
&self,
|
|
1325
1382
|
env: Env,
|
|
1326
1383
|
ids: Vec<u32>,
|
|
1327
1384
|
options_json: Option<String>,
|
|
1328
|
-
) -> Result<
|
|
1385
|
+
) -> Result<JsText> {
|
|
1329
1386
|
self.maybe_collect();
|
|
1330
1387
|
let overrides = match options_json {
|
|
1331
1388
|
Some(json) => Some(serde_json::from_str::<Value>(&json).map_err(json_error)?),
|
|
@@ -1333,7 +1390,7 @@ impl NativeDocument {
|
|
|
1333
1390
|
}
|
|
1334
1391
|
.filter(Value::is_object);
|
|
1335
1392
|
let rendered = self.render_nodes(&ids, overrides)?;
|
|
1336
|
-
Ok(self.reported(env, rendered))
|
|
1393
|
+
Ok(JsText(self.reported(env, rendered)))
|
|
1337
1394
|
}
|
|
1338
1395
|
|
|
1339
1396
|
fn render_nodes(&self, ids: &[u32], overrides: Option<Value>) -> Result<String> {
|
|
@@ -1364,7 +1421,11 @@ impl NativeDocument {
|
|
|
1364
1421
|
Ok(arena.serialize(ids, &arena::normalized_options(options)))
|
|
1365
1422
|
}
|
|
1366
1423
|
|
|
1367
|
-
#[napi]
|
|
1424
|
+
#[napi(js_name = "attr", ts_return_type = "string | null")]
|
|
1425
|
+
pub fn attr_for_js(&self, id: u32, name: String) -> Option<JsText> {
|
|
1426
|
+
self.attr(id, name).map(JsText)
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1368
1429
|
pub fn attr(&self, id: u32, name: String) -> Option<String> {
|
|
1369
1430
|
self.attribute(id, &name)
|
|
1370
1431
|
}
|
|
@@ -1382,9 +1443,9 @@ impl NativeDocument {
|
|
|
1382
1443
|
.flatten()
|
|
1383
1444
|
}
|
|
1384
1445
|
|
|
1385
|
-
#[napi(js_name = "exportTree")]
|
|
1386
|
-
pub fn export_tree_and_report(&self, env: Env) -> Result<
|
|
1387
|
-
self.reported(env, self.export_tree())
|
|
1446
|
+
#[napi(js_name = "exportTree", ts_return_type = "string")]
|
|
1447
|
+
pub fn export_tree_and_report(&self, env: Env) -> Result<JsText> {
|
|
1448
|
+
self.reported(env, self.export_tree()).map(JsText)
|
|
1388
1449
|
}
|
|
1389
1450
|
|
|
1390
1451
|
pub fn export_tree(&self) -> Result<String> {
|
|
@@ -1412,9 +1473,9 @@ impl NativeDocument {
|
|
|
1412
1473
|
built.into_js(&env)
|
|
1413
1474
|
}
|
|
1414
1475
|
|
|
1415
|
-
#[napi(js_name = "node")]
|
|
1416
|
-
pub fn node_and_report(&self, env: Env, id: u32) -> Result<
|
|
1417
|
-
self.reported(env, self.node(id))
|
|
1476
|
+
#[napi(js_name = "node", ts_return_type = "string")]
|
|
1477
|
+
pub fn node_and_report(&self, env: Env, id: u32) -> Result<JsText> {
|
|
1478
|
+
self.reported(env, self.node(id)).map(JsText)
|
|
1418
1479
|
}
|
|
1419
1480
|
|
|
1420
1481
|
pub fn node(&self, id: u32) -> Result<String> {
|
|
@@ -1563,9 +1624,9 @@ impl NativeDocument {
|
|
|
1563
1624
|
.map_err(json_error)
|
|
1564
1625
|
}
|
|
1565
1626
|
|
|
1566
|
-
#[napi(js_name = "op")]
|
|
1567
|
-
pub fn op_and_report(&self, env: Env, command: String, args_json: String) -> Result<
|
|
1568
|
-
self.reported(env, self.op(command, args_json))
|
|
1627
|
+
#[napi(js_name = "op", ts_return_type = "string")]
|
|
1628
|
+
pub fn op_and_report(&self, env: Env, command: String, args_json: String) -> Result<JsText> {
|
|
1629
|
+
self.reported(env, self.op(command, args_json)).map(JsText)
|
|
1569
1630
|
}
|
|
1570
1631
|
|
|
1571
1632
|
pub fn op(&self, command: String, args_json: String) -> Result<String> {
|
|
@@ -1671,7 +1732,11 @@ impl NativeDocument {
|
|
|
1671
1732
|
}
|
|
1672
1733
|
}
|
|
1673
1734
|
self.ensure_arena()?;
|
|
1674
|
-
let mut arena =
|
|
1735
|
+
let mut arena = if attributes::keeps_element_names(&command) {
|
|
1736
|
+
self.arena_for_attribute_write()
|
|
1737
|
+
} else {
|
|
1738
|
+
self.arena_for_write()
|
|
1739
|
+
};
|
|
1675
1740
|
let arena = arena.as_mut().unwrap();
|
|
1676
1741
|
let previous_options = arena.options.clone();
|
|
1677
1742
|
if let Some(options) = args.get("options").filter(|options| options.is_object()) {
|
|
@@ -2058,7 +2123,9 @@ fn convert_node(
|
|
|
2058
2123
|
.insert("prefix".into(), Value::String(String::from(&**prefix)));
|
|
2059
2124
|
}
|
|
2060
2125
|
let (mut namespaces, mut prefixes) = (Map::new(), Map::new());
|
|
2061
|
-
|
|
2126
|
+
if !element.attrs.is_empty() {
|
|
2127
|
+
*record.attribs = Map::with_capacity(element.attrs.len());
|
|
2128
|
+
}
|
|
2062
2129
|
for (name, value) in &element.attrs {
|
|
2063
2130
|
let key = String::from(&*name.local);
|
|
2064
2131
|
if !name.ns.is_empty() {
|
|
@@ -2299,6 +2366,9 @@ pub struct ParseManyTask {
|
|
|
2299
2366
|
is_document: bool,
|
|
2300
2367
|
threads: usize,
|
|
2301
2368
|
options: Value,
|
|
2369
|
+
/// Extraction plans to run on each root after parsing; with them, each
|
|
2370
|
+
/// document also gets its snapshot built on the parsing thread.
|
|
2371
|
+
plans: Option<Vec<Value>>,
|
|
2302
2372
|
}
|
|
2303
2373
|
|
|
2304
2374
|
#[napi]
|
|
@@ -2308,12 +2378,24 @@ impl Task for ParseManyTask {
|
|
|
2308
2378
|
|
|
2309
2379
|
fn compute(&mut self) -> Result<Self::Output> {
|
|
2310
2380
|
runtime::parallel_map(&self.htmls, self.threads, |html| {
|
|
2311
|
-
|
|
2312
|
-
html,
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2381
|
+
let document =
|
|
2382
|
+
NativeDocument::parse_options(html, self.is_document, self.options.clone(), None);
|
|
2383
|
+
if let Some(plans) = &self.plans {
|
|
2384
|
+
let results = plans
|
|
2385
|
+
.iter()
|
|
2386
|
+
.map(|plan| {
|
|
2387
|
+
let args =
|
|
2388
|
+
serde_json::json!({"ids": [0], "plan": plan, "options": self.options});
|
|
2389
|
+
document.op("extractPlan".into(), args.to_string()).ok()
|
|
2390
|
+
})
|
|
2391
|
+
.collect();
|
|
2392
|
+
*document.prepared_plans.borrow_mut() = Some(results);
|
|
2393
|
+
// With source locations the snapshot is built from the mutable tree instead.
|
|
2394
|
+
if document.source.borrow().is_none() {
|
|
2395
|
+
*document.prepared_snapshot.borrow_mut() = Some(snapshot::build(&document));
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
Ok(document)
|
|
2317
2399
|
})
|
|
2318
2400
|
}
|
|
2319
2401
|
|
|
@@ -2328,8 +2410,12 @@ pub fn parse_many_async(
|
|
|
2328
2410
|
is_document: bool,
|
|
2329
2411
|
threads: Option<u32>,
|
|
2330
2412
|
options_json: Option<String>,
|
|
2413
|
+
plans_json: Option<String>,
|
|
2331
2414
|
) -> Result<AsyncTask<ParseManyTask>> {
|
|
2332
2415
|
let threads = thread_count(threads, htmls.len())?;
|
|
2416
|
+
let plans = plans_json
|
|
2417
|
+
.map(|json| serde_json::from_str(&json).map_err(json_error))
|
|
2418
|
+
.transpose()?;
|
|
2333
2419
|
Ok(AsyncTask::new(ParseManyTask {
|
|
2334
2420
|
htmls,
|
|
2335
2421
|
is_document,
|
|
@@ -2337,6 +2423,7 @@ pub fn parse_many_async(
|
|
|
2337
2423
|
options: options_json
|
|
2338
2424
|
.and_then(|value| serde_json::from_str(&value).ok())
|
|
2339
2425
|
.unwrap_or_else(|| serde_json::json!({})),
|
|
2426
|
+
plans,
|
|
2340
2427
|
}))
|
|
2341
2428
|
}
|
|
2342
2429
|
|
package/native/src/snapshot.rs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//! which frees it as soon as the snapshot is collected; an external buffer
|
|
16
16
|
//! would add finalizer bookkeeping and wait for a later event-loop turn.
|
|
17
17
|
use html5ever::{LocalName, Namespace};
|
|
18
|
-
use napi::{Env, JsObject, JsTypedArray,
|
|
18
|
+
use napi::{check_status, sys, Env, JsObject, JsTypedArray, NapiValue, Result};
|
|
19
19
|
use scraper::Node;
|
|
20
20
|
use serde_json::{Map, Value};
|
|
21
21
|
|
|
@@ -24,8 +24,9 @@ use crate::id_hash::IdMap;
|
|
|
24
24
|
use crate::NativeDocument;
|
|
25
25
|
|
|
26
26
|
pub(crate) const STRIDE: usize = 15;
|
|
27
|
-
/// Text longer than this
|
|
28
|
-
/// scripts
|
|
27
|
+
/// Text or attribute values longer than this stay out of snapshots: such
|
|
28
|
+
/// nodes, mostly inline scripts, styles, SVG paths and embedded data, are read
|
|
29
|
+
/// individually if JavaScript asks for them.
|
|
29
30
|
const LARGE_TEXT_BYTES: usize = 4096;
|
|
30
31
|
const ABSENT: u32 = u32::MAX;
|
|
31
32
|
const HAS_START_INDEX: u32 = 1;
|
|
@@ -35,6 +36,17 @@ const EMPTY_ATTRIBUTE_MAPS: u32 = 8;
|
|
|
35
36
|
/// The record cannot be represented here; JavaScript reads it individually.
|
|
36
37
|
const OPAQUE: u32 = 16;
|
|
37
38
|
const RELEASED: u32 = 32;
|
|
39
|
+
const KINDS: [&str; 8] = [
|
|
40
|
+
"tag",
|
|
41
|
+
"text",
|
|
42
|
+
"comment",
|
|
43
|
+
"root",
|
|
44
|
+
"script",
|
|
45
|
+
"style",
|
|
46
|
+
"directive",
|
|
47
|
+
"cdata",
|
|
48
|
+
];
|
|
49
|
+
const SHORT_NAME_SLOTS: usize = 256;
|
|
38
50
|
|
|
39
51
|
pub(crate) struct NodeSnapshot {
|
|
40
52
|
is_encoded: bool,
|
|
@@ -57,90 +69,74 @@ impl NodeSnapshot {
|
|
|
57
69
|
u32::from(self.is_encoded),
|
|
58
70
|
];
|
|
59
71
|
let length = header.len() + parts.iter().map(|part| part.len()).sum::<usize>();
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for part in std::iter::once(&header[..]).chain(parts.iter().map(|part| part.as_slice())) {
|
|
69
|
-
words[offset..offset + part.len()].copy_from_slice(part);
|
|
70
|
-
offset += part.len();
|
|
71
|
-
}
|
|
72
|
-
let data: JsTypedArray =
|
|
73
|
-
buffer
|
|
74
|
-
.into_raw()
|
|
75
|
-
.into_typedarray(TypedArrayType::Uint32, length, 0)?;
|
|
72
|
+
let data = uninitialized_words(env, length, |words| {
|
|
73
|
+
let mut offset = 0;
|
|
74
|
+
for part in std::iter::once(&header[..]).chain(parts.iter().map(|part| part.as_slice()))
|
|
75
|
+
{
|
|
76
|
+
words[offset..offset + part.len()].copy_from_slice(part);
|
|
77
|
+
offset += part.len();
|
|
78
|
+
}
|
|
79
|
+
})?;
|
|
76
80
|
let mut object = env.create_object()?;
|
|
77
81
|
object.set_named_property("data", data)?;
|
|
78
|
-
|
|
79
|
-
// character fits, which also keeps V8's compact one-byte string.
|
|
80
|
-
let text = if self.text.is_ascii() {
|
|
81
|
-
env.create_string_latin1(self.text.as_bytes())?
|
|
82
|
-
} else if let Some(latin1) = latin1_bytes(&self.text) {
|
|
83
|
-
env.create_string_latin1(&latin1)?
|
|
84
|
-
} else {
|
|
85
|
-
env.create_string_utf16(&utf16_units(&self.text))?
|
|
86
|
-
};
|
|
87
|
-
object.set_named_property("text", text)?;
|
|
82
|
+
object.set_named_property("text", crate::codec::js_string(env, &self.text)?)?;
|
|
88
83
|
Ok(object)
|
|
89
84
|
}
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
/// `
|
|
93
|
-
///
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
),
|
|
133
|
-
};
|
|
134
|
-
if scalar < 0x1_0000 {
|
|
135
|
-
units.push(scalar as u16);
|
|
136
|
-
} else {
|
|
137
|
-
let offset = scalar - 0x1_0000;
|
|
138
|
-
units.push(0xD800 | (offset >> 10) as u16);
|
|
139
|
-
units.push(0xDC00 | (offset & 0x3FF) as u16);
|
|
87
|
+
/// A `Uint32Array` of `length` words that `fill` writes. A Node buffer is
|
|
88
|
+
/// used for its storage because, unlike an `ArrayBuffer`, it is not
|
|
89
|
+
/// zero-filled first; `fill` writes every word before JavaScript can read it.
|
|
90
|
+
fn uninitialized_words(
|
|
91
|
+
env: &Env,
|
|
92
|
+
length: usize,
|
|
93
|
+
fill: impl FnOnce(&mut [u32]),
|
|
94
|
+
) -> Result<JsTypedArray> {
|
|
95
|
+
let raw_env = env.raw();
|
|
96
|
+
let mut buffer = std::ptr::null_mut();
|
|
97
|
+
let mut memory = std::ptr::null_mut();
|
|
98
|
+
// SAFETY: Node-API calls on the current environment with valid out
|
|
99
|
+
// pointers. `memory` is the buffer's own allocation of `length` words,
|
|
100
|
+
// checked to be aligned, and the buffer stays alive in this handle scope.
|
|
101
|
+
unsafe {
|
|
102
|
+
check_status!(sys::napi_create_buffer(
|
|
103
|
+
raw_env,
|
|
104
|
+
length * 4,
|
|
105
|
+
&mut memory,
|
|
106
|
+
&mut buffer
|
|
107
|
+
))?;
|
|
108
|
+
let mut kind = 0;
|
|
109
|
+
let mut bytes = 0;
|
|
110
|
+
let mut view_memory = std::ptr::null_mut();
|
|
111
|
+
let mut arraybuffer = std::ptr::null_mut();
|
|
112
|
+
let mut byte_offset = 0;
|
|
113
|
+
check_status!(sys::napi_get_typedarray_info(
|
|
114
|
+
raw_env,
|
|
115
|
+
buffer,
|
|
116
|
+
&mut kind,
|
|
117
|
+
&mut bytes,
|
|
118
|
+
&mut view_memory,
|
|
119
|
+
&mut arraybuffer,
|
|
120
|
+
&mut byte_offset,
|
|
121
|
+
))?;
|
|
122
|
+
if !byte_offset.is_multiple_of(4)
|
|
123
|
+
|| !(memory as usize).is_multiple_of(4)
|
|
124
|
+
|| bytes < length * 4
|
|
125
|
+
{
|
|
126
|
+
return Err(napi::Error::from_reason("Unaligned snapshot buffer"));
|
|
140
127
|
}
|
|
141
|
-
|
|
128
|
+
let mut words = std::ptr::null_mut();
|
|
129
|
+
check_status!(sys::napi_create_typedarray(
|
|
130
|
+
raw_env,
|
|
131
|
+
sys::TypedarrayType::uint32_array,
|
|
132
|
+
length,
|
|
133
|
+
arraybuffer,
|
|
134
|
+
byte_offset,
|
|
135
|
+
&mut words,
|
|
136
|
+
))?;
|
|
137
|
+
fill(std::slice::from_raw_parts_mut(memory.cast::<u32>(), length));
|
|
138
|
+
Ok(JsTypedArray::from_raw_unchecked(raw_env, words))
|
|
142
139
|
}
|
|
143
|
-
units
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
/// The UTF-16 length of `text`: one unit per character, two for characters
|
|
@@ -159,6 +155,11 @@ struct Builder {
|
|
|
159
155
|
text: String,
|
|
160
156
|
units: u32,
|
|
161
157
|
interned: IdMap<String, u32>,
|
|
158
|
+
/// References of the common node types, resolved without hashing.
|
|
159
|
+
kinds: [Option<u32>; KINDS.len()],
|
|
160
|
+
/// Recently interned names of up to eight bytes, keyed by their bytes:
|
|
161
|
+
/// element and attribute names repeat across nodes.
|
|
162
|
+
short_names: Box<[(u64, u8, u32); SHORT_NAME_SLOTS]>,
|
|
162
163
|
/// html5ever names are atoms that hash and compare by identity.
|
|
163
164
|
local_names: IdMap<LocalName, u32>,
|
|
164
165
|
namespaces: IdMap<Namespace, u32>,
|
|
@@ -178,6 +179,8 @@ pub(crate) fn build(document: &NativeDocument) -> NodeSnapshot {
|
|
|
178
179
|
text: String::with_capacity(document.content_bytes.get()),
|
|
179
180
|
units: 0,
|
|
180
181
|
interned: IdMap::with_capacity_and_hasher(32, Default::default()),
|
|
182
|
+
kinds: [None; KINDS.len()],
|
|
183
|
+
short_names: Box::new([(0, 0, 0); SHORT_NAME_SLOTS]),
|
|
181
184
|
local_names: IdMap::with_capacity_and_hasher(32, Default::default()),
|
|
182
185
|
namespaces: IdMap::default(),
|
|
183
186
|
codec,
|
|
@@ -244,6 +247,40 @@ impl Builder {
|
|
|
244
247
|
(self.strings.len() / 2 - 1) as u32
|
|
245
248
|
}
|
|
246
249
|
|
|
250
|
+
fn kind(&mut self, kind: &str) -> u32 {
|
|
251
|
+
let Some(slot) = KINDS.iter().position(|known| *known == kind) else {
|
|
252
|
+
return self.interned(kind);
|
|
253
|
+
};
|
|
254
|
+
if let Some(reference) = self.kinds[slot] {
|
|
255
|
+
return reference;
|
|
256
|
+
}
|
|
257
|
+
let reference = self.interned(kind);
|
|
258
|
+
self.kinds[slot] = Some(reference);
|
|
259
|
+
reference
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn interned_name(&mut self, name: &str) -> u32 {
|
|
263
|
+
let bytes = name.as_bytes();
|
|
264
|
+
if bytes.len() > 8 {
|
|
265
|
+
return self.interned(name);
|
|
266
|
+
}
|
|
267
|
+
let mut word = [0; 8];
|
|
268
|
+
word[..bytes.len()].copy_from_slice(bytes);
|
|
269
|
+
let key = u64::from_le_bytes(word);
|
|
270
|
+
let length = bytes.len() as u8;
|
|
271
|
+
let slot = (key
|
|
272
|
+
.wrapping_add(u64::from(length))
|
|
273
|
+
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
|
274
|
+
>> 56) as usize;
|
|
275
|
+
let (cached_key, cached_length, cached) = self.short_names[slot];
|
|
276
|
+
if cached != 0 && cached_key == key && cached_length == length {
|
|
277
|
+
return cached - 1;
|
|
278
|
+
}
|
|
279
|
+
let reference = self.interned(name);
|
|
280
|
+
self.short_names[slot] = (key, length, reference + 1);
|
|
281
|
+
reference
|
|
282
|
+
}
|
|
283
|
+
|
|
247
284
|
fn interned(&mut self, value: &str) -> u32 {
|
|
248
285
|
if let Some(reference) = self.interned.get(value) {
|
|
249
286
|
return *reference;
|
|
@@ -279,7 +316,7 @@ impl Builder {
|
|
|
279
316
|
|
|
280
317
|
fn encode(&mut self, node: &DomNode) {
|
|
281
318
|
let mut fields = [0; STRIDE];
|
|
282
|
-
fields[0] = self.
|
|
319
|
+
fields[0] = self.kind(&node.kind);
|
|
283
320
|
fields[1] = pointer(node.parent);
|
|
284
321
|
fields[2] = pointer(node.prev);
|
|
285
322
|
fields[3] = pointer(node.next);
|
|
@@ -289,7 +326,7 @@ impl Builder {
|
|
|
289
326
|
fields[6] = node
|
|
290
327
|
.name
|
|
291
328
|
.as_deref()
|
|
292
|
-
.map_or(0, |name| self.
|
|
329
|
+
.map_or(0, |name| self.interned_name(name) + 1);
|
|
293
330
|
let mut flags = 0;
|
|
294
331
|
match node.data.as_deref() {
|
|
295
332
|
Some(data) if data.len() > LARGE_TEXT_BYTES => flags |= OPAQUE,
|
|
@@ -297,13 +334,22 @@ impl Builder {
|
|
|
297
334
|
None => {}
|
|
298
335
|
}
|
|
299
336
|
fields[8] = (self.attributes.len() / 2) as u32;
|
|
300
|
-
|
|
301
|
-
|
|
337
|
+
let has_large_attribute = node.attribs.values().any(|value| {
|
|
338
|
+
value
|
|
339
|
+
.as_str()
|
|
340
|
+
.is_some_and(|value| value.len() > LARGE_TEXT_BYTES)
|
|
341
|
+
});
|
|
342
|
+
if has_large_attribute {
|
|
343
|
+
flags |= OPAQUE;
|
|
344
|
+
} else {
|
|
345
|
+
fields[9] = node.attribs.len() as u32;
|
|
346
|
+
}
|
|
347
|
+
for (name, value) in node.attribs.iter().filter(|_| !has_large_attribute) {
|
|
302
348
|
let Some(value) = value.as_str() else {
|
|
303
349
|
flags |= OPAQUE;
|
|
304
350
|
break;
|
|
305
351
|
};
|
|
306
|
-
let name = self.
|
|
352
|
+
let name = self.interned_name(name);
|
|
307
353
|
let value = self.string(value);
|
|
308
354
|
self.attributes.extend_from_slice(&[name, value]);
|
|
309
355
|
}
|
|
@@ -454,12 +500,20 @@ impl Builder {
|
|
|
454
500
|
_ => tag_type,
|
|
455
501
|
};
|
|
456
502
|
fields[6] = self.local_name(&element.name.local) + 1;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
503
|
+
if element
|
|
504
|
+
.attrs
|
|
505
|
+
.iter()
|
|
506
|
+
.any(|(_, value)| value.len() > LARGE_TEXT_BYTES)
|
|
507
|
+
{
|
|
508
|
+
flags |= OPAQUE;
|
|
509
|
+
} else {
|
|
510
|
+
for (attribute, value) in &element.attrs {
|
|
511
|
+
let attribute = self.local_name(&attribute.local);
|
|
512
|
+
let value = self.string(value);
|
|
513
|
+
self.attributes.extend_from_slice(&[attribute, value]);
|
|
514
|
+
}
|
|
515
|
+
fields[9] = element.attrs.len() as u32;
|
|
461
516
|
}
|
|
462
|
-
fields[9] = element.attrs.len() as u32;
|
|
463
517
|
fields[13] = self.namespace(&element.name.ns);
|
|
464
518
|
flags |= HAS_NAMESPACE | EMPTY_ATTRIBUTE_MAPS;
|
|
465
519
|
}
|
|
@@ -474,6 +528,7 @@ impl Builder {
|
|
|
474
528
|
#[cfg(test)]
|
|
475
529
|
mod tests {
|
|
476
530
|
use super::*;
|
|
531
|
+
use crate::codec::{latin1_bytes, utf16_units};
|
|
477
532
|
|
|
478
533
|
#[test]
|
|
479
534
|
fn transcodes_text_like_the_standard_library() {
|