rusteerio 1.1.1 → 1.1.3
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 +119 -30
- package/native/src/runtime.rs +23 -0
- package/native/src/snapshot.rs +117 -80
- 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/dist/slim.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { contains, dom, merge } from './index.mjs';
|
|
2
|
-
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-
|
|
3
|
-
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-
|
|
2
|
+
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-CJsc0PH-.mjs';
|
|
3
|
+
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-CJsc0PH-.mjs';
|
|
4
4
|
import 'node:stream';
|
|
5
5
|
import 'node:querystring';
|
|
6
6
|
import 'node:buffer';
|
package/dist/slim.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { contains, dom, merge } from './index.js';
|
|
2
|
-
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-
|
|
3
|
-
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-
|
|
2
|
+
import { L as LoadContent, C as CheerioOptions, a as CheerioAPI } from './types-CJsc0PH-.js';
|
|
3
|
+
export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-CJsc0PH-.js';
|
|
4
4
|
import 'node:stream';
|
|
5
5
|
import 'node:querystring';
|
|
6
6
|
import 'node:buffer';
|
package/dist/slim.mjs
CHANGED
|
@@ -2,6 +2,16 @@ import * as node_querystring from 'node:querystring';
|
|
|
2
2
|
import * as node_stream from 'node:stream';
|
|
3
3
|
import * as node_buffer from 'node:buffer';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The compact record copy returned by `NativeDocument.snapshot()`: the
|
|
7
|
+
* lengths of the node, child, attribute and string lists, whether any string
|
|
8
|
+
* holds codec escapes, then the lists.
|
|
9
|
+
*/
|
|
10
|
+
interface NativeSnapshot {
|
|
11
|
+
data: Uint32Array;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
interface NativeDocument {
|
|
6
16
|
readonly nodeSpace?: object;
|
|
7
17
|
retainNode?(id: number): void;
|
|
@@ -17,6 +27,8 @@ interface NativeDocument {
|
|
|
17
27
|
tagName(id: number): string | undefined | null;
|
|
18
28
|
exportTree(): string;
|
|
19
29
|
rootId(): number;
|
|
30
|
+
takePreparedPlans?(): (string | null)[] | null;
|
|
31
|
+
takePreparedSnapshot?(isWanted: boolean): NativeSnapshot | null;
|
|
20
32
|
parserDiagnostics?(): string;
|
|
21
33
|
}
|
|
22
34
|
interface NativeWorkerPoolInfo {
|
|
@@ -2,6 +2,16 @@ import * as node_querystring from 'node:querystring';
|
|
|
2
2
|
import * as node_stream from 'node:stream';
|
|
3
3
|
import * as node_buffer from 'node:buffer';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The compact record copy returned by `NativeDocument.snapshot()`: the
|
|
7
|
+
* lengths of the node, child, attribute and string lists, whether any string
|
|
8
|
+
* holds codec escapes, then the lists.
|
|
9
|
+
*/
|
|
10
|
+
interface NativeSnapshot {
|
|
11
|
+
data: Uint32Array;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
interface NativeDocument {
|
|
6
16
|
readonly nodeSpace?: object;
|
|
7
17
|
retainNode?(id: number): void;
|
|
@@ -17,6 +27,8 @@ interface NativeDocument {
|
|
|
17
27
|
tagName(id: number): string | undefined | null;
|
|
18
28
|
exportTree(): string;
|
|
19
29
|
rootId(): number;
|
|
30
|
+
takePreparedPlans?(): (string | null)[] | null;
|
|
31
|
+
takePreparedSnapshot?(isWanted: boolean): NativeSnapshot | null;
|
|
20
32
|
parserDiagnostics?(): string;
|
|
21
33
|
}
|
|
22
34
|
interface NativeWorkerPoolInfo {
|
package/dist/utils.d.mts
CHANGED
package/dist/utils.d.ts
CHANGED
package/native/src/arena.rs
CHANGED
|
@@ -17,7 +17,7 @@ pub(crate) struct DomNode {
|
|
|
17
17
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
18
18
|
pub data: Option<String>,
|
|
19
19
|
#[serde(default)]
|
|
20
|
-
pub attribs:
|
|
20
|
+
pub attribs: FieldMap,
|
|
21
21
|
#[serde(default)]
|
|
22
22
|
pub children: Vec<u32>,
|
|
23
23
|
#[serde(default)]
|
|
@@ -43,7 +43,94 @@ pub(crate) struct DomNode {
|
|
|
43
43
|
)]
|
|
44
44
|
pub attribs_prefix: Option<Box<Map<String, Value>>>,
|
|
45
45
|
#[serde(flatten)]
|
|
46
|
-
pub extras:
|
|
46
|
+
pub extras: FieldMap,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// A string-keyed map that most nodes leave empty, boxed so that an empty
|
|
50
|
+
/// one costs a pointer instead of a whole map in every node.
|
|
51
|
+
#[derive(Clone, Debug, Default)]
|
|
52
|
+
pub(crate) struct FieldMap(Option<Box<Map<String, Value>>>);
|
|
53
|
+
|
|
54
|
+
static EMPTY_FIELDS: std::sync::LazyLock<Map<String, Value>> = std::sync::LazyLock::new(Map::new);
|
|
55
|
+
|
|
56
|
+
impl std::ops::Deref for FieldMap {
|
|
57
|
+
type Target = Map<String, Value>;
|
|
58
|
+
fn deref(&self) -> &Self::Target {
|
|
59
|
+
self.0.as_deref().unwrap_or(&EMPTY_FIELDS)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl std::ops::DerefMut for FieldMap {
|
|
64
|
+
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
65
|
+
self.0.get_or_insert_with(Box::default)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl From<Map<String, Value>> for FieldMap {
|
|
70
|
+
fn from(map: Map<String, Value>) -> Self {
|
|
71
|
+
Self((!map.is_empty()).then(|| Box::new(map)))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Removals and lookups for writing leave an empty map unallocated; other
|
|
76
|
+
/// writes go through `DerefMut`, which allocates it.
|
|
77
|
+
impl FieldMap {
|
|
78
|
+
pub(crate) fn into_map(self) -> Map<String, Value> {
|
|
79
|
+
self.0.map_or_else(Map::new, |map| *map)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub(crate) fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
|
|
83
|
+
self.0.as_mut()?.get_mut(key)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub(crate) fn remove(&mut self, key: &str) -> Option<Value> {
|
|
87
|
+
self.0.as_mut()?.remove(key)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub(crate) fn shift_remove(&mut self, key: &str) -> Option<Value> {
|
|
91
|
+
self.0.as_mut()?.shift_remove(key)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl<'a> IntoIterator for &'a FieldMap {
|
|
96
|
+
type Item = (&'a String, &'a Value);
|
|
97
|
+
type IntoIter = serde_json::map::Iter<'a>;
|
|
98
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
99
|
+
(**self).iter()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl<'a> IntoIterator for &'a mut FieldMap {
|
|
104
|
+
type Item = (&'a String, &'a mut Value);
|
|
105
|
+
type IntoIter = serde_json::map::IterMut<'a>;
|
|
106
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
107
|
+
(**self).iter_mut()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl IntoIterator for FieldMap {
|
|
112
|
+
type Item = (String, Value);
|
|
113
|
+
type IntoIter = serde_json::map::IntoIter;
|
|
114
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
115
|
+
self.into_map().into_iter()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl Serialize for FieldMap {
|
|
120
|
+
fn serialize<S: serde::Serializer>(
|
|
121
|
+
&self,
|
|
122
|
+
serializer: S,
|
|
123
|
+
) -> std::result::Result<S::Ok, S::Error> {
|
|
124
|
+
(**self).serialize(serializer)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl<'de> Deserialize<'de> for FieldMap {
|
|
129
|
+
fn deserialize<D: serde::Deserializer<'de>>(
|
|
130
|
+
deserializer: D,
|
|
131
|
+
) -> std::result::Result<Self, D::Error> {
|
|
132
|
+
Map::deserialize(deserializer).map(Self::from)
|
|
133
|
+
}
|
|
47
134
|
}
|
|
48
135
|
|
|
49
136
|
fn root_kind() -> Cow<'static, str> {
|
|
@@ -90,7 +177,7 @@ impl DomNode {
|
|
|
90
177
|
kind: kind.into(),
|
|
91
178
|
name: None,
|
|
92
179
|
data: None,
|
|
93
|
-
attribs:
|
|
180
|
+
attribs: FieldMap::default(),
|
|
94
181
|
children: Vec::new(),
|
|
95
182
|
parent: None,
|
|
96
183
|
prev: None,
|
|
@@ -98,7 +185,7 @@ impl DomNode {
|
|
|
98
185
|
namespace: None,
|
|
99
186
|
attribs_namespace: None,
|
|
100
187
|
attribs_prefix: None,
|
|
101
|
-
extras:
|
|
188
|
+
extras: FieldMap::default(),
|
|
102
189
|
}
|
|
103
190
|
}
|
|
104
191
|
pub(crate) fn is_element(&self) -> bool {
|
|
@@ -722,7 +809,10 @@ impl DomArena {
|
|
|
722
809
|
pub(crate) fn insert(&mut self, targets: &[u32], ids: &[u32], position: &str) -> Vec<u32> {
|
|
723
810
|
let mut inserted = Vec::new();
|
|
724
811
|
for (index, target) in targets.iter().enumerate() {
|
|
725
|
-
|
|
812
|
+
// As Cheerio, only nodes that can hold children are targets.
|
|
813
|
+
if !self.node(*target).is_some_and(|node| {
|
|
814
|
+
node.is_element() || matches!(node.kind.as_ref(), "root" | "cdata")
|
|
815
|
+
}) {
|
|
726
816
|
continue;
|
|
727
817
|
}
|
|
728
818
|
if matches!(position, "append" | "prepend")
|
|
@@ -885,7 +975,7 @@ impl DomArena {
|
|
|
885
975
|
"name" | "tagName" => node.name = value.as_str().map(str::to_owned),
|
|
886
976
|
"data" | "nodeValue" => node.data = value.as_str().map(str::to_owned),
|
|
887
977
|
"type" => node.kind = node_kind(value.as_str().unwrap_or("tag")),
|
|
888
|
-
"attribs" => node.attribs = value.as_object().cloned().unwrap_or_default(),
|
|
978
|
+
"attribs" => node.attribs = value.as_object().cloned().unwrap_or_default().into(),
|
|
889
979
|
"parent" => node.parent = value.as_u64().map(|id| id as u32),
|
|
890
980
|
"prev" => node.prev = value.as_u64().map(|id| id as u32),
|
|
891
981
|
"next" => node.next = value.as_u64().map(|id| id as u32),
|
|
@@ -1131,6 +1221,72 @@ pub(crate) fn append_parsed(
|
|
|
1131
1221
|
id
|
|
1132
1222
|
}
|
|
1133
1223
|
|
|
1224
|
+
/// Appends a fresh node of `kind` to `parent` and returns its id. The record
|
|
1225
|
+
/// is built in the arena itself: moving a whole record through the callers
|
|
1226
|
+
/// copied it several times per node.
|
|
1227
|
+
pub(crate) fn append_parsed_new(
|
|
1228
|
+
arena: &mut DomArena,
|
|
1229
|
+
parent: u32,
|
|
1230
|
+
kind: &'static str,
|
|
1231
|
+
start: usize,
|
|
1232
|
+
end: usize,
|
|
1233
|
+
) -> u32 {
|
|
1234
|
+
let (start_indices, end_indices) = parse_index_flags(arena);
|
|
1235
|
+
let previous = arena.nodes[parent as usize].children.last().copied();
|
|
1236
|
+
let id = arena.push(DomNode::new(kind));
|
|
1237
|
+
let node = &mut arena.nodes[id as usize];
|
|
1238
|
+
node.parent = Some(parent);
|
|
1239
|
+
node.prev = previous;
|
|
1240
|
+
if start_indices {
|
|
1241
|
+
node.extras.insert("startIndex".into(), json!(start));
|
|
1242
|
+
}
|
|
1243
|
+
if end_indices {
|
|
1244
|
+
node.extras.insert("endIndex".into(), json!(end));
|
|
1245
|
+
}
|
|
1246
|
+
if let Some(previous) = previous {
|
|
1247
|
+
arena.nodes[previous as usize].next = Some(id);
|
|
1248
|
+
}
|
|
1249
|
+
arena.nodes[parent as usize].children.push(id);
|
|
1250
|
+
id
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/// Whether parsing records start and end indices, read once per parse.
|
|
1254
|
+
fn parse_index_flags(arena: &mut DomArena) -> (bool, bool) {
|
|
1255
|
+
*arena.parse_indices.get_or_insert_with(|| {
|
|
1256
|
+
(
|
|
1257
|
+
arena.options.get("withStartIndices") == Some(&Value::Bool(true)),
|
|
1258
|
+
arena.options.get("withEndIndices") == Some(&Value::Bool(true)),
|
|
1259
|
+
)
|
|
1260
|
+
})
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/// Appends parsed text to `parent`, merging it into a preceding text node.
|
|
1264
|
+
pub(crate) fn append_parsed_text(
|
|
1265
|
+
arena: &mut DomArena,
|
|
1266
|
+
parent: u32,
|
|
1267
|
+
data: String,
|
|
1268
|
+
start: usize,
|
|
1269
|
+
end: usize,
|
|
1270
|
+
) -> u32 {
|
|
1271
|
+
let previous_text = arena.nodes[parent as usize]
|
|
1272
|
+
.children
|
|
1273
|
+
.last()
|
|
1274
|
+
.copied()
|
|
1275
|
+
.filter(|id| arena.nodes[*id as usize].kind == "text");
|
|
1276
|
+
if let Some(previous) = previous_text {
|
|
1277
|
+
let (_, end_indices) = parse_index_flags(arena);
|
|
1278
|
+
let node = &mut arena.nodes[previous as usize];
|
|
1279
|
+
node.data.get_or_insert_with(String::new).push_str(&data);
|
|
1280
|
+
if end_indices {
|
|
1281
|
+
node.extras.insert("endIndex".into(), json!(end));
|
|
1282
|
+
}
|
|
1283
|
+
return previous;
|
|
1284
|
+
}
|
|
1285
|
+
let id = append_parsed_new(arena, parent, "text", start, end);
|
|
1286
|
+
arena.nodes[id as usize].data = Some(data);
|
|
1287
|
+
id
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1134
1290
|
pub(crate) fn html_parser_void(name: &str) -> bool {
|
|
1135
1291
|
(super::void_name(name) && name != "bgsound") || matches!(name, "command" | "isindex")
|
|
1136
1292
|
}
|
|
@@ -1297,7 +1453,7 @@ pub(crate) fn primitive(arena: &mut DomArena, command: &str, args: &Value) -> Re
|
|
|
1297
1453
|
Value::Object(
|
|
1298
1454
|
arena
|
|
1299
1455
|
.node(id)
|
|
1300
|
-
.map(|node| node.attribs.clone())
|
|
1456
|
+
.map(|node| (*node.attribs).clone())
|
|
1301
1457
|
.unwrap_or_default(),
|
|
1302
1458
|
)
|
|
1303
1459
|
}
|
package/native/src/attributes.rs
CHANGED
|
@@ -94,7 +94,7 @@ fn get_attr(arena: &DomArena, id: u32, name: &str) -> Value {
|
|
|
94
94
|
return Value::Null;
|
|
95
95
|
};
|
|
96
96
|
if name.is_empty() {
|
|
97
|
-
return Value::Object(node.attribs.clone());
|
|
97
|
+
return Value::Object((*node.attribs).clone());
|
|
98
98
|
}
|
|
99
99
|
if let Some(value) = node.attribs.get(name) {
|
|
100
100
|
return if !arena.xml_mode() && boolean_attribute(name) {
|
|
@@ -570,6 +570,23 @@ fn encode_component(value: &str, encoded: bool) -> Result<String, ()> {
|
|
|
570
570
|
Ok(output)
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
pub(crate) fn keeps_element_names(command: &str) -> bool {
|
|
574
|
+
matches!(
|
|
575
|
+
command,
|
|
576
|
+
"attr"
|
|
577
|
+
| "removeAttr"
|
|
578
|
+
| "css"
|
|
579
|
+
| "data"
|
|
580
|
+
| "removeData"
|
|
581
|
+
| "hasClass"
|
|
582
|
+
| "addClass"
|
|
583
|
+
| "removeClass"
|
|
584
|
+
| "toggleClass"
|
|
585
|
+
| "serializeArray"
|
|
586
|
+
| "serialize"
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
573
590
|
pub(crate) fn execute(
|
|
574
591
|
arena: &mut DomArena,
|
|
575
592
|
command: &str,
|
package/native/src/codec.rs
CHANGED
|
@@ -1,7 +1,86 @@
|
|
|
1
1
|
//! Lossless JavaScript UTF-16 over valid Rust strings. Ordinary Unicode keeps
|
|
2
2
|
//! its original representation; lone surrogates use BMP private-use frames.
|
|
3
|
+
use napi::{Env, JsString, NapiRaw};
|
|
3
4
|
use serde_json::Value;
|
|
4
5
|
|
|
6
|
+
/// A string returned to JavaScript. V8 decodes UTF-8 slowly, so text is
|
|
7
|
+
/// handed over as Latin-1 when every character fits, which also keeps V8's
|
|
8
|
+
/// compact one-byte string, and as UTF-16 otherwise.
|
|
9
|
+
pub struct JsText(pub String);
|
|
10
|
+
|
|
11
|
+
impl napi::bindgen_prelude::ToNapiValue for JsText {
|
|
12
|
+
unsafe fn to_napi_value(
|
|
13
|
+
env: napi::sys::napi_env,
|
|
14
|
+
value: Self,
|
|
15
|
+
) -> napi::Result<napi::sys::napi_value> {
|
|
16
|
+
Ok(js_string(&Env::from_raw(env), &value.0)?.raw())
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub(crate) fn js_string(env: &Env, text: &str) -> napi::Result<JsString> {
|
|
21
|
+
if text.is_ascii() {
|
|
22
|
+
env.create_string_latin1(text.as_bytes())
|
|
23
|
+
} else if let Some(latin1) = latin1_bytes(text) {
|
|
24
|
+
env.create_string_latin1(&latin1)
|
|
25
|
+
} else {
|
|
26
|
+
env.create_string_utf16(&utf16_units(text))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// `text` in Latin-1, if every character is below U+0100: UTF-8 encodes
|
|
31
|
+
/// those with lead bytes below 0xC4.
|
|
32
|
+
pub(crate) fn latin1_bytes(text: &str) -> Option<Vec<u8>> {
|
|
33
|
+
if text.bytes().any(|byte| byte >= 0xC4) {
|
|
34
|
+
return None;
|
|
35
|
+
}
|
|
36
|
+
Some(text.chars().map(|character| character as u8).collect())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// `text` as UTF-16, widening eight ASCII bytes at a time and decoding other
|
|
40
|
+
/// characters directly from their UTF-8 bytes, which `text` guarantees valid.
|
|
41
|
+
pub(crate) fn utf16_units(text: &str) -> Vec<u16> {
|
|
42
|
+
const HIGH_BITS: u64 = 0x8080_8080_8080_8080;
|
|
43
|
+
let bytes = text.as_bytes();
|
|
44
|
+
let mut units = Vec::with_capacity(bytes.len());
|
|
45
|
+
let mut index = 0;
|
|
46
|
+
while index < bytes.len() {
|
|
47
|
+
if let Some(chunk) = bytes.get(index..index + 8) {
|
|
48
|
+
let word = u64::from_le_bytes(chunk.try_into().unwrap());
|
|
49
|
+
if word & HIGH_BITS == 0 {
|
|
50
|
+
units.extend(chunk.iter().map(|byte| u16::from(*byte)));
|
|
51
|
+
index += 8;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let lead = u32::from(bytes[index]);
|
|
56
|
+
let continuation = |offset: usize| u32::from(bytes[index + offset] & 0x3F);
|
|
57
|
+
let (scalar, length) = match lead {
|
|
58
|
+
0x00..=0x7F => (lead, 1),
|
|
59
|
+
0x80..=0xDF => (((lead & 0x1F) << 6) | continuation(1), 2),
|
|
60
|
+
0xE0..=0xEF => (
|
|
61
|
+
((lead & 0x0F) << 12) | (continuation(1) << 6) | continuation(2),
|
|
62
|
+
3,
|
|
63
|
+
),
|
|
64
|
+
_ => (
|
|
65
|
+
((lead & 0x07) << 18)
|
|
66
|
+
| (continuation(1) << 12)
|
|
67
|
+
| (continuation(2) << 6)
|
|
68
|
+
| continuation(3),
|
|
69
|
+
4,
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
if scalar < 0x1_0000 {
|
|
73
|
+
units.push(scalar as u16);
|
|
74
|
+
} else {
|
|
75
|
+
let offset = scalar - 0x1_0000;
|
|
76
|
+
units.push(0xD800 | (offset >> 10) as u16);
|
|
77
|
+
units.push(0xDC00 | (offset & 0x3FF) as u16);
|
|
78
|
+
}
|
|
79
|
+
index += length;
|
|
80
|
+
}
|
|
81
|
+
units
|
|
82
|
+
}
|
|
83
|
+
|
|
5
84
|
pub(crate) const ESCAPE: char = '\u{e800}';
|
|
6
85
|
pub(crate) fn is_reserved(ch: char) -> bool {
|
|
7
86
|
('\u{e000}'..=ESCAPE).contains(&ch)
|