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.
Files changed (46) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +37 -37
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/dist/{chunk-FPRP6UIX.mjs → chunk-HN63P3U7.mjs} +625 -401
  6. package/dist/chunk-HN63P3U7.mjs.map +1 -0
  7. package/dist/index.cjs +624 -400
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.mts +3 -2
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/slim.cjs +555 -407
  13. package/dist/slim.cjs.map +1 -1
  14. package/dist/slim.d.mts +2 -2
  15. package/dist/slim.d.ts +2 -2
  16. package/dist/slim.mjs +1 -1
  17. package/dist/{types-Cs9UP1Vv.d.mts → types-CJsc0PH-.d.mts} +12 -0
  18. package/dist/{types-Cs9UP1Vv.d.ts → types-CJsc0PH-.d.ts} +12 -0
  19. package/dist/utils.d.mts +1 -1
  20. package/dist/utils.d.ts +1 -1
  21. package/native/src/arena.rs +163 -7
  22. package/native/src/attributes.rs +18 -1
  23. package/native/src/codec.rs +79 -0
  24. package/native/src/lib.rs +119 -30
  25. package/native/src/runtime.rs +23 -0
  26. package/native/src/snapshot.rs +117 -80
  27. package/native/src/tokenizer.rs +34 -20
  28. package/package.json +1 -1
  29. package/rusteerio.darwin-arm64.node +0 -0
  30. package/rusteerio.darwin-x64.node +0 -0
  31. package/rusteerio.linux-arm64-gnu.node +0 -0
  32. package/rusteerio.linux-arm64-musl.node +0 -0
  33. package/rusteerio.linux-x64-gnu.node +0 -0
  34. package/rusteerio.linux-x64-musl.node +0 -0
  35. package/rusteerio.win32-arm64-msvc.node +0 -0
  36. package/rusteerio.win32-x64-msvc.node +0 -0
  37. package/src/extract.ts +117 -24
  38. package/src/index.ts +27 -6
  39. package/src/match.ts +4 -4
  40. package/src/native.ts +4 -0
  41. package/src/nodes.ts +70 -10
  42. package/src/router.ts +80 -4
  43. package/src/selection.ts +18 -17
  44. package/src/snapshot.ts +37 -0
  45. package/src/transport.ts +1 -1
  46. 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-Cs9UP1Vv.mjs';
3
- export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-Cs9UP1Vv.mjs';
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-Cs9UP1Vv.js';
3
- export { A as AcceptedElems, b as AcceptedFilters, c as AnyNode, B as BasicAcceptedElems, d as CDATA, type e as Cheerio, f as CheerioRequestOptions, g as ChildNode, h as Comment, D as DataNode, i as DecodeStreamOptions, j as Document, k as DocumentMode, E as Element, type l as ElementType, m as EncodingOptions, n as ExtractDescriptor, o as ExtractMap, p as ExtractedMap, q as ExtractedValue, r as ExtractionDescriptor, s as ExtractionMap, t as ExtractionResult, F as FilterFunction, u as FormDataLike, H as HTMLParser2Options, I as InternalOptions, N as NamespaceURI, v as Node, w as NodeWithChildren, P as ParallelOptions, x as ParentNode, y as ParserError, z as ParserErrorCode, G as ParserErrorHandler, J as ProcessingInstruction, S as SelectorType, K as SerializedField, M as SourceCodeLocation, T as TagSourceCodeLocation, O as Text, Q as Tokenizer, R as TokenizerCallbacks, U as TokenizerConstructor, V as TokenizerOptions, W as TreeAdapter, X as TreeAdapterAttribute } from './types-Cs9UP1Vv.js';
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
@@ -3,7 +3,7 @@ import {
3
3
  dom,
4
4
  load,
5
5
  merge
6
- } from "./chunk-FPRP6UIX.mjs";
6
+ } from "./chunk-HN63P3U7.mjs";
7
7
  import "./chunk-OHPGEY3E.mjs";
8
8
 
9
9
  // src/slim.ts
@@ -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
@@ -1,4 +1,4 @@
1
- import { e as Cheerio, c as AnyNode, E as Element } from './types-Cs9UP1Vv.mjs';
1
+ import { e as Cheerio, c as AnyNode, E as Element } from './types-CJsc0PH-.mjs';
2
2
  import 'node:querystring';
3
3
  import 'node:stream';
4
4
  import 'node:buffer';
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { e as Cheerio, c as AnyNode, E as Element } from './types-Cs9UP1Vv.js';
1
+ import { e as Cheerio, c as AnyNode, E as Element } from './types-CJsc0PH-.js';
2
2
  import 'node:querystring';
3
3
  import 'node:stream';
4
4
  import 'node:buffer';
@@ -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: Map<String, Value>,
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: Map<String, Value>,
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: Map::new(),
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: Map::new(),
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
- if self.node(*target).is_none() {
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
  }
@@ -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,
@@ -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)