rusteerio 0.0.0-stage → 0.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.
Files changed (206) hide show
  1. package/CONTRIBUTING.md +73 -0
  2. package/Cargo.lock +1051 -0
  3. package/Cargo.toml +36 -0
  4. package/LICENSE +21 -0
  5. package/README.md +252 -2
  6. package/RELEASING.md +138 -0
  7. package/THIRD_PARTY_NOTICES.md +7443 -0
  8. package/build.rs +3 -0
  9. package/dist/chunk-DKULX6VO.mjs +3185 -0
  10. package/dist/chunk-DKULX6VO.mjs.map +1 -0
  11. package/dist/chunk-OHPGEY3E.mjs +32 -0
  12. package/dist/chunk-OHPGEY3E.mjs.map +1 -0
  13. package/dist/index.cjs +3230 -0
  14. package/dist/index.cjs.map +1 -0
  15. package/dist/index.d.mts +113 -0
  16. package/dist/index.d.ts +113 -0
  17. package/dist/index.mjs +38 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/slim.cjs +2441 -0
  20. package/dist/slim.cjs.map +1 -0
  21. package/dist/slim.d.mts +11 -0
  22. package/dist/slim.d.ts +11 -0
  23. package/dist/slim.mjs +19 -0
  24. package/dist/slim.mjs.map +1 -0
  25. package/dist/types-BP6TpxE5.d.mts +532 -0
  26. package/dist/types-BP6TpxE5.d.ts +532 -0
  27. package/dist/utils.cjs +61 -0
  28. package/dist/utils.cjs.map +1 -0
  29. package/dist/utils.d.mts +13 -0
  30. package/dist/utils.d.ts +13 -0
  31. package/dist/utils.mjs +17 -0
  32. package/dist/utils.mjs.map +1 -0
  33. package/native/src/arena.rs +1781 -0
  34. package/native/src/attributes.rs +923 -0
  35. package/native/src/codec.rs +172 -0
  36. package/native/src/diagnostics.rs +1249 -0
  37. package/native/src/extraction.rs +226 -0
  38. package/native/src/gc.rs +166 -0
  39. package/native/src/hook_sink.rs +727 -0
  40. package/native/src/hook_stream.rs +513 -0
  41. package/native/src/lib.rs +2095 -0
  42. package/native/src/location.rs +392 -0
  43. package/native/src/runtime.rs +189 -0
  44. package/native/src/selector.rs +1287 -0
  45. package/native/src/sink.rs +261 -0
  46. package/native/src/stream.rs +334 -0
  47. package/native/src/tokenizer.rs +355 -0
  48. package/native/src/tracker.rs +232 -0
  49. package/native/vendor/README.md +25 -0
  50. package/native/vendor/html5ever-0.35.0/.cargo_vcs_info.json +6 -0
  51. package/native/vendor/html5ever-0.35.0/Cargo.lock +828 -0
  52. package/native/vendor/html5ever-0.35.0/Cargo.toml +85 -0
  53. package/native/vendor/html5ever-0.35.0/LICENSE-APACHE +201 -0
  54. package/native/vendor/html5ever-0.35.0/LICENSE-MIT +25 -0
  55. package/native/vendor/html5ever-0.35.0/README.md +60 -0
  56. package/native/vendor/html5ever-0.35.0/benches/html5ever.rs +86 -0
  57. package/native/vendor/html5ever-0.35.0/data/bench/lipsum-zh.html +19 -0
  58. package/native/vendor/html5ever-0.35.0/data/bench/lipsum.html +40 -0
  59. package/native/vendor/html5ever-0.35.0/data/bench/medium-fragment.html +24 -0
  60. package/native/vendor/html5ever-0.35.0/data/bench/small-fragment.html +7 -0
  61. package/native/vendor/html5ever-0.35.0/data/bench/strong.html +1 -0
  62. package/native/vendor/html5ever-0.35.0/data/bench/tiny-fragment.html +1 -0
  63. package/native/vendor/html5ever-0.35.0/examples/arena.rs +356 -0
  64. package/native/vendor/html5ever-0.35.0/examples/capi/tokenize.c +74 -0
  65. package/native/vendor/html5ever-0.35.0/examples/noop-tokenize.rs +47 -0
  66. package/native/vendor/html5ever-0.35.0/examples/noop-tree-builder.rs +132 -0
  67. package/native/vendor/html5ever-0.35.0/examples/print-tree-actions.rs +185 -0
  68. package/native/vendor/html5ever-0.35.0/examples/tokenize.rs +111 -0
  69. package/native/vendor/html5ever-0.35.0/src/driver.rs +148 -0
  70. package/native/vendor/html5ever-0.35.0/src/lib.rs +30 -0
  71. package/native/vendor/html5ever-0.35.0/src/macros.rs +34 -0
  72. package/native/vendor/html5ever-0.35.0/src/serialize/mod.rs +255 -0
  73. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +451 -0
  74. package/native/vendor/html5ever-0.35.0/src/tokenizer/interface.rs +99 -0
  75. package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +2305 -0
  76. package/native/vendor/html5ever-0.35.0/src/tokenizer/states.rs +97 -0
  77. package/native/vendor/html5ever-0.35.0/src/tree_builder/data.rs +170 -0
  78. package/native/vendor/html5ever-0.35.0/src/tree_builder/mod.rs +1848 -0
  79. package/native/vendor/html5ever-0.35.0/src/tree_builder/rules.rs +1494 -0
  80. package/native/vendor/html5ever-0.35.0/src/tree_builder/tag_sets.rs +114 -0
  81. package/native/vendor/html5ever-0.35.0/src/tree_builder/types.rs +91 -0
  82. package/native/vendor/html5ever-0.35.0/src/util/str.rs +52 -0
  83. package/native/vendor/html5gum-0.8.4/Cargo.lock +1006 -0
  84. package/native/vendor/html5gum-0.8.4/Cargo.toml +109 -0
  85. package/native/vendor/html5gum-0.8.4/LICENSE +19 -0
  86. package/native/vendor/html5gum-0.8.4/README.md +110 -0
  87. package/native/vendor/html5gum-0.8.4/benches/files/0050.html +2131 -0
  88. package/native/vendor/html5gum-0.8.4/benches/files/6D30.html +1004 -0
  89. package/native/vendor/html5gum-0.8.4/benches/files.rs +32 -0
  90. package/native/vendor/html5gum-0.8.4/benches/patterns.rs +31 -0
  91. package/native/vendor/html5gum-0.8.4/src/arrayvec.rs +29 -0
  92. package/native/vendor/html5gum-0.8.4/src/char_validator.rs +134 -0
  93. package/native/vendor/html5gum-0.8.4/src/emitters/callback.rs +676 -0
  94. package/native/vendor/html5gum-0.8.4/src/emitters/default.rs +207 -0
  95. package/native/vendor/html5gum-0.8.4/src/emitters/emitter.rs +344 -0
  96. package/native/vendor/html5gum-0.8.4/src/emitters/html5ever.rs +306 -0
  97. package/native/vendor/html5gum-0.8.4/src/emitters/mod.rs +25 -0
  98. package/native/vendor/html5gum-0.8.4/src/entities.rs +3342 -0
  99. package/native/vendor/html5gum-0.8.4/src/error.rs +96 -0
  100. package/native/vendor/html5gum-0.8.4/src/htmlstring.rs +130 -0
  101. package/native/vendor/html5gum-0.8.4/src/lib.rs +66 -0
  102. package/native/vendor/html5gum-0.8.4/src/machine.rs +2136 -0
  103. package/native/vendor/html5gum-0.8.4/src/machine_helper.rs +277 -0
  104. package/native/vendor/html5gum-0.8.4/src/read_helper.rs +244 -0
  105. package/native/vendor/html5gum-0.8.4/src/reader.rs +440 -0
  106. package/native/vendor/html5gum-0.8.4/src/span.rs +98 -0
  107. package/native/vendor/html5gum-0.8.4/src/state.rs +16 -0
  108. package/native/vendor/html5gum-0.8.4/src/testutils.rs +71 -0
  109. package/native/vendor/html5gum-0.8.4/src/tokenizer.rs +189 -0
  110. package/native/vendor/html5gum-0.8.4/src/utils.rs +88 -0
  111. package/native/vendor/markup5ever-0.35.0/.cargo_vcs_info.json +6 -0
  112. package/native/vendor/markup5ever-0.35.0/Cargo.toml +45 -0
  113. package/native/vendor/markup5ever-0.35.0/LICENSE-APACHE +201 -0
  114. package/native/vendor/markup5ever-0.35.0/LICENSE-MIT +25 -0
  115. package/native/vendor/markup5ever-0.35.0/interface/mod.rs +422 -0
  116. package/native/vendor/markup5ever-0.35.0/interface/tree_builder.rs +280 -0
  117. package/native/vendor/markup5ever-0.35.0/lib.rs +59 -0
  118. package/native/vendor/markup5ever-0.35.0/serialize.rs +77 -0
  119. package/native/vendor/markup5ever-0.35.0/util/buffer_queue.rs +333 -0
  120. package/native/vendor/markup5ever-0.35.0/util/smallcharset.rs +88 -0
  121. package/package.json +137 -4
  122. package/rusteerio.darwin-arm64.node +0 -0
  123. package/rusteerio.darwin-x64.node +0 -0
  124. package/rusteerio.linux-arm64-gnu.node +0 -0
  125. package/rusteerio.linux-arm64-musl.node +0 -0
  126. package/rusteerio.linux-x64-gnu.node +0 -0
  127. package/rusteerio.linux-x64-musl.node +0 -0
  128. package/rusteerio.win32-arm64-msvc.node +0 -0
  129. package/rusteerio.win32-x64-msvc.node +0 -0
  130. package/scripts/check-native.mjs +15 -0
  131. package/scripts/check-release.mjs +235 -0
  132. package/scripts/fix-declarations.mjs +27 -0
  133. package/scripts/generate-notices.mjs +176 -0
  134. package/scripts/notices/README.md +13 -0
  135. package/scripts/notices/cheerio-MIT.txt +21 -0
  136. package/scripts/notices/html5ever-AUTHORS.txt +20 -0
  137. package/scripts/notices/html5ever-COPYRIGHT.txt +8 -0
  138. package/scripts/notices/html5ever-LICENSE-APACHE.txt +201 -0
  139. package/scripts/notices/html5ever-LICENSE-MIT.txt +25 -0
  140. package/scripts/notices/napi-rs-MIT.txt +43 -0
  141. package/scripts/notices/neon-MIT.txt +19 -0
  142. package/scripts/notices/runtime-01c266bced4a434da005.txt +3 -0
  143. package/scripts/notices/runtime-0a54e647fe54104658b5.txt +14 -0
  144. package/scripts/notices/runtime-0b74dfa0bcee5c420c6b.txt +25 -0
  145. package/scripts/notices/runtime-0d542e0c8804e39aa7f3.txt +177 -0
  146. package/scripts/notices/runtime-0f96a83840e146e43c0e.txt +21 -0
  147. package/scripts/notices/runtime-123a331b5dbf04c30097.txt +25 -0
  148. package/scripts/notices/runtime-172020dbfd5b53a226df.txt +34 -0
  149. package/scripts/notices/runtime-209fbbe0ad52d9235e37.txt +26 -0
  150. package/scripts/notices/runtime-23f18e03dc49df91622f.txt +23 -0
  151. package/scripts/notices/runtime-268872b9816f90fd8e85.txt +220 -0
  152. package/scripts/notices/runtime-35242e7a83f69875e6ed.txt +176 -0
  153. package/scripts/notices/runtime-378f5840b258e2779c39.txt +25 -0
  154. package/scripts/notices/runtime-4108245a1f2df9d4e94d.txt +25 -0
  155. package/scripts/notices/runtime-4455bf75a91154108304.txt +21 -0
  156. package/scripts/notices/runtime-553fffcd9b1cb158bc3e.txt +13 -0
  157. package/scripts/notices/runtime-62c7a1e35f56406896d7.txt +176 -0
  158. package/scripts/notices/runtime-6485b8ed310d3f0340bf.txt +25 -0
  159. package/scripts/notices/runtime-69fef7b0f322a6555415.txt +201 -0
  160. package/scripts/notices/runtime-6df43f6f4b5d4587f3d8.txt +187 -0
  161. package/scripts/notices/runtime-799e9ca9d179295ef372.txt +24 -0
  162. package/scripts/notices/runtime-7b63ecd5f1902af1b637.txt +25 -0
  163. package/scripts/notices/runtime-7bc7d5de2a0b793d4e1a.txt +21 -0
  164. package/scripts/notices/runtime-7e12e5df4bae12cb2158.txt +24 -0
  165. package/scripts/notices/runtime-861399f8c21c042b1105.txt +12 -0
  166. package/scripts/notices/runtime-8ada45cd9f843acf64e4.txt +201 -0
  167. package/scripts/notices/runtime-90eb64f0279b0d9432ac.txt +12 -0
  168. package/scripts/notices/runtime-a60eea817514531668d7.txt +201 -0
  169. package/scripts/notices/runtime-a7c936ff1ed8fa340172.txt +25 -0
  170. package/scripts/notices/runtime-ab6eec6caf0fa5775e41.txt +275 -0
  171. package/scripts/notices/runtime-b1181a40b2a7b25cf66f.txt +19 -0
  172. package/scripts/notices/runtime-b71bd43a069ca0641a9e.txt +25 -0
  173. package/scripts/notices/runtime-c16f8dcf1a368b83be78.txt +201 -0
  174. package/scripts/notices/runtime-c2cfccb812fe482101a8.txt +21 -0
  175. package/scripts/notices/runtime-e99d88d232bf57d70f0f.txt +25 -0
  176. package/scripts/notices/runtime-f32fb3b417a194167cfa.txt +9 -0
  177. package/scripts/notices/runtime-f5062c9a188d81dfe66b.txt +39 -0
  178. package/scripts/notices/runtime-f9070a832833fa7488fd.txt +4 -0
  179. package/scripts/notices/runtime-fab3dd6bdab226f1c086.txt +373 -0
  180. package/scripts/notices/runtime-fba3318d0c600380177e.txt +62 -0
  181. package/scripts/notices/runtime-ff8f68cb076caf8cefe7.txt +25 -0
  182. package/scripts/notices/runtime-ff92bed461f50338dd70.txt +74 -0
  183. package/scripts/notices/runtime.json +1632 -0
  184. package/scripts/notices/sources.json +555 -0
  185. package/scripts/notices/xray-MIT.txt +21 -0
  186. package/src/attributes.ts +271 -0
  187. package/src/codec.ts +74 -0
  188. package/src/dom.ts +87 -0
  189. package/src/extract-plan.ts +97 -0
  190. package/src/extract.ts +130 -0
  191. package/src/hooks.ts +220 -0
  192. package/src/index.ts +223 -0
  193. package/src/io.ts +491 -0
  194. package/src/native.ts +135 -0
  195. package/src/nodes.ts +290 -0
  196. package/src/parse.ts +53 -0
  197. package/src/parser-types.ts +157 -0
  198. package/src/router.ts +327 -0
  199. package/src/selection.ts +566 -0
  200. package/src/slim.ts +12 -0
  201. package/src/transport.ts +104 -0
  202. package/src/types.ts +186 -0
  203. package/src/utils.ts +29 -0
  204. package/src/workers.ts +21 -0
  205. package/tsconfig.json +15 -0
  206. package/tsup.config.ts +16 -0
@@ -0,0 +1,1781 @@
1
+ //! Mutable, stable-ID DOM storage. All mutations stay on the native side.
2
+ use std::collections::{HashMap, HashSet};
3
+
4
+ use napi::{Error, Result, Status};
5
+ use serde::{Deserialize, Serialize};
6
+ use serde_json::{json, Map, Value};
7
+
8
+ #[derive(Clone, Debug, Serialize, Deserialize)]
9
+ pub(crate) struct DomNode {
10
+ #[serde(default)]
11
+ pub id: u32,
12
+ #[serde(rename = "type", default = "root_kind")]
13
+ pub kind: String,
14
+ #[serde(default, skip_serializing_if = "Option::is_none")]
15
+ pub name: Option<String>,
16
+ #[serde(default, skip_serializing_if = "Option::is_none")]
17
+ pub data: Option<String>,
18
+ #[serde(default)]
19
+ pub attribs: Map<String, Value>,
20
+ #[serde(default)]
21
+ pub children: Vec<u32>,
22
+ #[serde(default)]
23
+ pub parent: Option<u32>,
24
+ #[serde(default)]
25
+ pub prev: Option<u32>,
26
+ #[serde(default)]
27
+ pub next: Option<u32>,
28
+ #[serde(flatten)]
29
+ pub extras: Map<String, Value>,
30
+ }
31
+
32
+ fn root_kind() -> String {
33
+ "root".into()
34
+ }
35
+
36
+ impl DomNode {
37
+ pub(crate) fn new(kind: &str) -> Self {
38
+ Self {
39
+ id: 0,
40
+ kind: kind.into(),
41
+ name: None,
42
+ data: None,
43
+ attribs: Map::new(),
44
+ children: Vec::new(),
45
+ parent: None,
46
+ prev: None,
47
+ next: None,
48
+ extras: Map::new(),
49
+ }
50
+ }
51
+ pub(crate) fn is_element(&self) -> bool {
52
+ matches!(self.kind.as_str(), "tag" | "script" | "style")
53
+ }
54
+ }
55
+
56
+ #[derive(Clone, Debug)]
57
+ pub(crate) struct DomArena {
58
+ pub nodes: Vec<DomNode>,
59
+ pub options: Value,
60
+ pub free: Vec<u32>,
61
+ }
62
+
63
+ impl DomArena {
64
+ pub(crate) fn from_records(mut nodes: Vec<DomNode>, options: Value) -> Self {
65
+ for (id, node) in nodes.iter_mut().enumerate() {
66
+ node.id = id as u32;
67
+ node.extras.entry("startIndex").or_insert(Value::Null);
68
+ node.extras.entry("endIndex").or_insert(Value::Null);
69
+ }
70
+ let mut arena = Self {
71
+ nodes,
72
+ options,
73
+ free: Vec::new(),
74
+ };
75
+ for id in 0..arena.nodes.len() {
76
+ arena.relink(id as u32);
77
+ }
78
+ arena
79
+ }
80
+ pub(crate) fn node(&self, id: u32) -> Option<&DomNode> {
81
+ self.nodes
82
+ .get(id as usize)
83
+ .filter(|node| node.kind != "released")
84
+ }
85
+ pub(crate) fn xml_mode(&self) -> bool {
86
+ xml_mode(&self.options)
87
+ }
88
+ pub(crate) fn push(&mut self, mut node: DomNode) -> u32 {
89
+ while let Some(id) = self.free.pop() {
90
+ if self
91
+ .nodes
92
+ .get(id as usize)
93
+ .is_some_and(|old| old.kind == "released")
94
+ {
95
+ node.id = id;
96
+ self.nodes[id as usize] = node;
97
+ return id;
98
+ }
99
+ }
100
+ let id = self.nodes.len() as u32;
101
+ node.id = id;
102
+ self.nodes.push(node);
103
+ id
104
+ }
105
+ pub(crate) fn attr(&self, id: u32, name: &str) -> Option<&str> {
106
+ self.node(id)?.attribs.get(name)?.as_str()
107
+ }
108
+ pub(crate) fn set_attr(&mut self, ids: &[u32], name: &str, value: Option<&str>) {
109
+ for id in ids {
110
+ if let Some(node) = self.nodes.get_mut(*id as usize) {
111
+ if node.is_element() {
112
+ match value {
113
+ Some(value) => {
114
+ node.attribs
115
+ .insert(name.into(), Value::String(value.into()));
116
+ }
117
+ None => {
118
+ node.attribs.remove(name);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ pub(crate) fn relink(&mut self, parent: u32) {
126
+ let children = self
127
+ .nodes
128
+ .get(parent as usize)
129
+ .map(|node| node.children.clone())
130
+ .unwrap_or_default();
131
+ for (index, child) in children.iter().enumerate() {
132
+ if let Some(node) = self.nodes.get_mut(*child as usize) {
133
+ node.parent = Some(parent);
134
+ node.prev = index
135
+ .checked_sub(1)
136
+ .and_then(|index| children.get(index))
137
+ .copied();
138
+ node.next = children.get(index + 1).copied();
139
+ }
140
+ }
141
+ }
142
+ pub(crate) fn detach(&mut self, id: u32) {
143
+ if let Some(parent) = self.node(id).and_then(|node| node.parent) {
144
+ if let Some(node) = self.nodes.get_mut(parent as usize) {
145
+ node.children.retain(|child| *child != id);
146
+ }
147
+ self.relink(parent);
148
+ }
149
+ if let Some(node) = self.nodes.get_mut(id as usize) {
150
+ node.parent = None;
151
+ node.prev = None;
152
+ node.next = None;
153
+ }
154
+ }
155
+ pub(crate) fn detach_many(&mut self, ids: &[u32]) {
156
+ if ids.len() < 2 {
157
+ if let Some(id) = ids.first() {
158
+ self.detach(*id);
159
+ }
160
+ return;
161
+ }
162
+ let removed: HashSet<_> = ids.iter().copied().collect();
163
+ let parents: HashSet<_> = ids
164
+ .iter()
165
+ .filter_map(|id| self.node(*id).and_then(|node| node.parent))
166
+ .collect();
167
+ for parent in parents {
168
+ if let Some(node) = self.nodes.get_mut(parent as usize) {
169
+ node.children.retain(|child| !removed.contains(child));
170
+ }
171
+ self.relink(parent);
172
+ }
173
+ for id in removed {
174
+ if let Some(node) = self.nodes.get_mut(id as usize) {
175
+ node.parent = None;
176
+ node.prev = None;
177
+ node.next = None;
178
+ }
179
+ }
180
+ }
181
+ pub(crate) fn set_children(&mut self, parent: u32, children: &[u32]) {
182
+ if self.node(parent).is_none() {
183
+ return;
184
+ }
185
+ if !self
186
+ .node(parent)
187
+ .is_some_and(|node| node.is_element() || node.kind == "root" || node.kind == "cdata")
188
+ {
189
+ return;
190
+ }
191
+ let old = std::mem::take(&mut self.nodes[parent as usize].children);
192
+ self.detach_many(&old);
193
+ let mut seen = HashSet::new();
194
+ let mut children: Vec<_> = children
195
+ .iter()
196
+ .rev()
197
+ .copied()
198
+ .filter(|id| *id != parent && self.node(*id).is_some() && seen.insert(*id))
199
+ .collect();
200
+ children.reverse();
201
+ self.detach_many(&children);
202
+ self.nodes[parent as usize].children = children;
203
+ self.relink(parent);
204
+ }
205
+ pub(crate) fn text(&self, ids: &[u32]) -> String {
206
+ let mut output = String::new();
207
+ for id in ids {
208
+ let mut stack = vec![*id];
209
+ let mut visited = HashSet::new();
210
+ while let Some(id) = stack.pop() {
211
+ if !visited.insert(id) {
212
+ continue;
213
+ }
214
+ if let Some(node) = self.node(id) {
215
+ if node.kind == "text" {
216
+ output.push_str(node.data.as_deref().unwrap_or(""));
217
+ } else {
218
+ stack.extend(node.children.iter().rev().copied());
219
+ }
220
+ }
221
+ }
222
+ }
223
+ output
224
+ }
225
+ pub(crate) fn set_text(&mut self, ids: &[u32], value: &str) {
226
+ for id in ids {
227
+ if self
228
+ .node(*id)
229
+ .is_some_and(|node| node.is_element() || node.kind == "root")
230
+ {
231
+ self.set_children(*id, &[]);
232
+ if !value.is_empty() {
233
+ let mut text = DomNode::new("text");
234
+ text.data = Some(value.into());
235
+ let child = self.push(text);
236
+ self.set_children(*id, &[child]);
237
+ }
238
+ }
239
+ }
240
+ }
241
+ pub(crate) fn inner_html(&self, id: u32) -> String {
242
+ self.outer_html(
243
+ self.node(id)
244
+ .map(|node| node.children.as_slice())
245
+ .unwrap_or(&[]),
246
+ )
247
+ }
248
+ pub(crate) fn inner_html_mut(&mut self, id: u32) -> String {
249
+ if !self.xml_mode() && self.options.get("_useHtmlParser2") != Some(&Value::Bool(true)) {
250
+ if let Some(node) = self.node(id) {
251
+ let mut children = node.children.clone();
252
+ let mut index = 0;
253
+ while index < children.len() {
254
+ if let Some(root) = self
255
+ .node(children[index])
256
+ .filter(|node| node.kind == "root")
257
+ {
258
+ let contents = root.children.clone();
259
+ children.splice(index..index + 1, contents);
260
+ }
261
+ index += 1;
262
+ }
263
+ self.nodes[id as usize].children = children;
264
+ }
265
+ }
266
+ self.inner_html(id)
267
+ }
268
+ pub(crate) fn outer_html(&self, ids: &[u32]) -> String {
269
+ self.serialize(ids, &self.options)
270
+ }
271
+ pub(crate) fn serialize(&self, ids: &[u32], options: &Value) -> String {
272
+ let mut output = String::new();
273
+ let xml = xml_mode(options);
274
+ let encode = options.get("decodeEntities") != Some(&Value::Bool(false))
275
+ && options.get("encodeEntities") != Some(&Value::Bool(false));
276
+ let mut stack: Vec<(u32, bool)> = ids.iter().rev().map(|id| (*id, false)).collect();
277
+ let mut budget = self.nodes.len().saturating_mul(8).saturating_add(1024);
278
+ while let Some((id, close)) = stack.pop() {
279
+ if budget == 0 {
280
+ break;
281
+ }
282
+ budget -= 1;
283
+ let Some(node) = self.node(id) else {
284
+ continue;
285
+ };
286
+ if close {
287
+ output.push_str("</");
288
+ output.push_str(node.name.as_deref().unwrap_or(""));
289
+ output.push('>');
290
+ continue;
291
+ }
292
+ match node.kind.as_str() {
293
+ "root" => stack.extend(node.children.iter().rev().map(|id| (*id, false))),
294
+ "text" => {
295
+ let raw = !xml
296
+ && node
297
+ .parent
298
+ .and_then(|id| self.node(id))
299
+ .is_some_and(|parent| {
300
+ matches!(
301
+ parent.name.as_deref(),
302
+ Some(
303
+ "script"
304
+ | "style"
305
+ | "xmp"
306
+ | "iframe"
307
+ | "noembed"
308
+ | "noframes"
309
+ | "plaintext"
310
+ | "noscript"
311
+ )
312
+ ) && (parent.name.as_deref() != Some("noscript")
313
+ || options.get("scriptingEnabled") != Some(&Value::Bool(false)))
314
+ });
315
+ if raw || !encode {
316
+ output.push_str(node.data.as_deref().unwrap_or(""));
317
+ } else {
318
+ escape(
319
+ node.data.as_deref().unwrap_or(""),
320
+ false,
321
+ xml || options.get("_useHtmlParser2") == Some(&Value::Bool(true)),
322
+ options.get("encodeEntities").and_then(Value::as_str) == Some("utf8"),
323
+ &mut output,
324
+ );
325
+ }
326
+ }
327
+ "comment" => {
328
+ output.push_str("<!--");
329
+ output.push_str(node.data.as_deref().unwrap_or(""));
330
+ output.push_str("-->");
331
+ }
332
+ "cdata" => {
333
+ output.push_str("<![CDATA[");
334
+ output.push_str(&self.text(&node.children));
335
+ output.push_str("]]>");
336
+ }
337
+ "directive" | "doctype" => {
338
+ output.push('<');
339
+ if !xml && node.name.as_deref() == Some("!doctype") {
340
+ output.push_str("!DOCTYPE ");
341
+ output.push_str(
342
+ node.extras
343
+ .get("x-name")
344
+ .and_then(Value::as_str)
345
+ .unwrap_or("html"),
346
+ );
347
+ } else {
348
+ output.push_str(node.data.as_deref().unwrap_or(""));
349
+ }
350
+ output.push('>');
351
+ }
352
+ _ if node.is_element() => {
353
+ let name = node.name.as_deref().unwrap_or("");
354
+ output.push('<');
355
+ output.push_str(name);
356
+ for (name, value) in &node.attribs {
357
+ output.push(' ');
358
+ if !xml {
359
+ if let Some(prefix) = node
360
+ .extras
361
+ .get("attribsPrefix")
362
+ .and_then(|value| value.get(name))
363
+ .and_then(Value::as_str)
364
+ {
365
+ output.push_str(prefix);
366
+ output.push(':');
367
+ }
368
+ }
369
+ output.push_str(name);
370
+ let value = value
371
+ .as_str()
372
+ .map(str::to_owned)
373
+ .unwrap_or_else(|| value.to_string());
374
+ if !xml
375
+ && options.get("_useHtmlParser2") == Some(&Value::Bool(true))
376
+ && options.get("emptyAttrs") != Some(&Value::Bool(true))
377
+ && value.is_empty()
378
+ {
379
+ continue;
380
+ }
381
+ output.push_str("=\"");
382
+ if encode {
383
+ escape(
384
+ &value,
385
+ true,
386
+ xml || options.get("_useHtmlParser2") == Some(&Value::Bool(true)),
387
+ options.get("encodeEntities").and_then(Value::as_str)
388
+ == Some("utf8"),
389
+ &mut output,
390
+ );
391
+ } else {
392
+ output.push_str(&value.replace('"', "&quot;"));
393
+ }
394
+ output.push('"');
395
+ }
396
+ let is_void = !xml
397
+ && (node
398
+ .extras
399
+ .get("namespace")
400
+ .and_then(Value::as_str)
401
+ .is_none_or(|namespace| namespace == super::HTML_NS))
402
+ && super::void_name(name);
403
+ if xml
404
+ && node.children.is_empty()
405
+ && options.get("selfClosingTags") != Some(&Value::Bool(false))
406
+ {
407
+ output.push_str("/>");
408
+ continue;
409
+ }
410
+ output.push('>');
411
+ if is_void {
412
+ continue;
413
+ }
414
+ stack.push((id, true));
415
+ if !xml
416
+ && options.get("_useHtmlParser2") != Some(&Value::Bool(true))
417
+ && name == "template"
418
+ {
419
+ if let Some(contents) = node.children.first().and_then(|id| self.node(*id))
420
+ {
421
+ stack.extend(contents.children.iter().rev().map(|id| (*id, false)));
422
+ }
423
+ } else {
424
+ stack.extend(node.children.iter().rev().map(|id| (*id, false)));
425
+ }
426
+ }
427
+ _ => {}
428
+ }
429
+ }
430
+ output
431
+ }
432
+ pub(crate) fn clone_ids(&mut self, ids: &[u32], deep: bool) -> Vec<u32> {
433
+ ids.iter()
434
+ .filter_map(|id| self.clone_branch(*id, deep))
435
+ .collect()
436
+ }
437
+ fn clone_branch(&mut self, root: u32, deep: bool) -> Option<u32> {
438
+ let mut node = self.node(root)?.clone();
439
+ if node.is_element() {
440
+ node.extras.remove("data");
441
+ }
442
+ let source_children = node.children.clone();
443
+ node.children.clear();
444
+ node.parent = None;
445
+ node.prev = None;
446
+ node.next = None;
447
+ let new_root = self.push(node);
448
+ if deep {
449
+ let mut queue = vec![(new_root, source_children)];
450
+ while let Some((parent, children)) = queue.pop() {
451
+ for child in children {
452
+ if let Some(node) = self.node(child) {
453
+ let mut copy = node.clone();
454
+ if copy.is_element() {
455
+ copy.extras.remove("data");
456
+ }
457
+ let child_children = copy.children.clone();
458
+ copy.children.clear();
459
+ copy.parent = None;
460
+ copy.prev = None;
461
+ copy.next = None;
462
+ let id = self.push(copy);
463
+ self.nodes[parent as usize].children.push(id);
464
+ queue.push((id, child_children));
465
+ }
466
+ }
467
+ self.relink(parent);
468
+ }
469
+ }
470
+ Some(new_root)
471
+ }
472
+ pub(crate) fn import(&mut self, records: &[DomNode], roots: &[u32]) -> Vec<u32> {
473
+ self.import_internal(records, roots, true)
474
+ }
475
+ fn import_internal(
476
+ &mut self,
477
+ records: &[DomNode],
478
+ roots: &[u32],
479
+ detach_roots: bool,
480
+ ) -> Vec<u32> {
481
+ self.nodes.reserve(records.len());
482
+ let mut mapping = HashMap::new();
483
+ for record in records {
484
+ let mut node = record.clone();
485
+ node.children.clear();
486
+ node.parent = None;
487
+ node.prev = None;
488
+ node.next = None;
489
+ let id = self.push(node);
490
+ mapping.insert(record.id, id);
491
+ }
492
+ for record in records {
493
+ if let Some(id) = mapping.get(&record.id) {
494
+ self.nodes[*id as usize].children = record
495
+ .children
496
+ .iter()
497
+ .filter_map(|child| mapping.get(child).copied())
498
+ .collect();
499
+ self.relink(*id);
500
+ }
501
+ }
502
+ let roots: Vec<_> = roots
503
+ .iter()
504
+ .filter_map(|root| mapping.get(root).copied())
505
+ .collect();
506
+ if detach_roots {
507
+ self.detach_many(&roots);
508
+ }
509
+ roots
510
+ }
511
+ pub(crate) fn create_fragment(&mut self, html: &str, context: Option<u32>) -> Result<Vec<u32>> {
512
+ let context_name = context
513
+ .and_then(|id| self.node(id))
514
+ .and_then(|node| node.name.clone());
515
+ let mut options = self.options.clone();
516
+ if let Some(namespace) = context
517
+ .and_then(|id| self.node(id))
518
+ .and_then(|node| node.extras.get("namespace"))
519
+ .cloned()
520
+ {
521
+ options["__contextNamespace"] = namespace;
522
+ }
523
+ let document =
524
+ super::NativeDocument::parse_options(html, false, options, context_name.as_deref());
525
+ document.ensure_arena()?;
526
+ let arena = document.arena.borrow();
527
+ let parsed = arena.as_ref().unwrap();
528
+ let roots = parsed.nodes[0].children.clone();
529
+ Ok(self.import_internal(&parsed.nodes, &roots, false))
530
+ }
531
+ pub(crate) fn set_html(&mut self, ids: &[u32], html: &str) -> Result<()> {
532
+ for id in ids {
533
+ let children = self.create_fragment(html, Some(*id))?;
534
+ self.set_children(*id, &children);
535
+ }
536
+ Ok(())
537
+ }
538
+ pub(crate) fn insert(&mut self, targets: &[u32], ids: &[u32], position: &str) -> Vec<u32> {
539
+ let mut inserted = Vec::new();
540
+ for (index, target) in targets.iter().enumerate() {
541
+ if self.node(*target).is_none() {
542
+ continue;
543
+ }
544
+ if matches!(position, "append" | "prepend")
545
+ && !self
546
+ .node(*target)
547
+ .is_some_and(|node| node.is_element() || node.kind == "root")
548
+ {
549
+ continue;
550
+ }
551
+ let ids = if index + 1 == targets.len() {
552
+ ids.to_vec()
553
+ } else {
554
+ self.clone_ids(ids, true)
555
+ };
556
+ let (parent, mut offset) = if matches!(position, "append" | "prepend") {
557
+ (
558
+ *target,
559
+ if position == "append" {
560
+ self.nodes[*target as usize].children.len()
561
+ } else {
562
+ 0
563
+ },
564
+ )
565
+ } else {
566
+ let Some(parent) = self.nodes[*target as usize].parent else {
567
+ continue;
568
+ };
569
+ let at = self.nodes[parent as usize]
570
+ .children
571
+ .iter()
572
+ .position(|id| id == target)
573
+ .unwrap_or(0);
574
+ (parent, at + usize::from(position == "after"))
575
+ };
576
+ let valid: Vec<_> = ids
577
+ .iter()
578
+ .copied()
579
+ .filter(|id| *id != parent && self.node(*id).is_some())
580
+ .collect();
581
+ let removed: HashSet<_> = valid.iter().copied().collect();
582
+ offset -= self.nodes[parent as usize]
583
+ .children
584
+ .iter()
585
+ .take(offset)
586
+ .filter(|id| removed.contains(id))
587
+ .count();
588
+ self.detach_many(&valid);
589
+ let mut seen = HashSet::new();
590
+ let mut moved: Vec<_> = valid
591
+ .iter()
592
+ .rev()
593
+ .copied()
594
+ .filter(|id| seen.insert(*id))
595
+ .collect();
596
+ moved.reverse();
597
+ let offset = offset.min(self.nodes[parent as usize].children.len());
598
+ self.nodes[parent as usize]
599
+ .children
600
+ .splice(offset..offset, moved);
601
+ inserted.extend(valid);
602
+ self.relink(parent);
603
+ if position == "replace" && !ids.contains(target) {
604
+ self.detach(*target);
605
+ }
606
+ }
607
+ inserted
608
+ }
609
+ pub(crate) fn traverse(&self, ids: &[u32], kind: &str) -> Vec<u32> {
610
+ let mut result = Vec::new();
611
+ for id in ids {
612
+ let Some(node) = self.node(*id) else {
613
+ continue;
614
+ };
615
+ match kind {
616
+ "children" => result.extend(
617
+ node.children
618
+ .iter()
619
+ .copied()
620
+ .filter(|id| self.node(*id).is_some_and(DomNode::is_element)),
621
+ ),
622
+ "contents" => result.extend(node.children.iter().copied()),
623
+ "parent" => result.extend(
624
+ node.parent
625
+ .filter(|id| self.node(*id).is_some_and(DomNode::is_element)),
626
+ ),
627
+ "parents" | "ancestors" => {
628
+ let mut parent = node.parent;
629
+ let mut seen = HashSet::new();
630
+ while let Some(id) = parent {
631
+ if !seen.insert(id) {
632
+ break;
633
+ }
634
+ if self.node(id).is_some_and(DomNode::is_element) {
635
+ result.push(id);
636
+ }
637
+ parent = self.node(id).and_then(|node| node.parent);
638
+ }
639
+ }
640
+ "next" | "prev" | "nextAll" | "prevAll" => {
641
+ let forward = kind.starts_with("next");
642
+ let mut next = if forward { node.next } else { node.prev };
643
+ let mut seen = HashSet::new();
644
+ while let Some(id) = next {
645
+ if !seen.insert(id) {
646
+ break;
647
+ }
648
+ if self.node(id).is_some_and(DomNode::is_element) {
649
+ result.push(id);
650
+ if kind == "next" || kind == "prev" {
651
+ break;
652
+ }
653
+ }
654
+ next = self
655
+ .node(id)
656
+ .and_then(|node| if forward { node.next } else { node.prev });
657
+ }
658
+ }
659
+ "siblings" => {
660
+ if let Some(parent) = node.parent.and_then(|parent| self.node(parent)) {
661
+ result.extend(parent.children.iter().copied().filter(|other| {
662
+ other != id && self.node(*other).is_some_and(DomNode::is_element)
663
+ }));
664
+ }
665
+ }
666
+ "descendants" => {
667
+ let mut stack: Vec<_> = node.children.iter().rev().copied().collect();
668
+ while let Some(id) = stack.pop() {
669
+ if let Some(node) = self.node(id) {
670
+ if node.is_element() {
671
+ result.push(id);
672
+ }
673
+ stack.extend(node.children.iter().rev().copied());
674
+ }
675
+ }
676
+ }
677
+ _ => {}
678
+ }
679
+ }
680
+ result
681
+ }
682
+ pub(crate) fn set_node(&mut self, id: u32, field: &str, value: &Value) -> Result<()> {
683
+ if self.node(id).is_none() {
684
+ return Ok(());
685
+ }
686
+ if field == "children" {
687
+ self.set_children(id, &value_ids(value));
688
+ return Ok(());
689
+ }
690
+ let node = &mut self.nodes[id as usize];
691
+ match field {
692
+ "name" | "tagName" => node.name = value.as_str().map(str::to_owned),
693
+ "data" | "nodeValue" => node.data = value.as_str().map(str::to_owned),
694
+ "type" => node.kind = value.as_str().unwrap_or("tag").into(),
695
+ "attribs" => node.attribs = value.as_object().cloned().unwrap_or_default(),
696
+ "parent" => node.parent = value.as_u64().map(|id| id as u32),
697
+ "prev" => node.prev = value.as_u64().map(|id| id as u32),
698
+ "next" => node.next = value.as_u64().map(|id| id as u32),
699
+ _ => {
700
+ if value.is_null() {
701
+ node.extras.remove(field);
702
+ } else {
703
+ node.extras.insert(field.into(), value.clone());
704
+ }
705
+ }
706
+ }
707
+ Ok(())
708
+ }
709
+ }
710
+
711
+ impl super::selector::SelectDom for DomArena {
712
+ fn kind(&self, id: u32) -> &str {
713
+ self.node(id).map_or("", |node| node.kind.as_str())
714
+ }
715
+ fn name(&self, id: u32) -> Option<&str> {
716
+ self.node(id)?.name.as_deref()
717
+ }
718
+ fn attr(&self, id: u32, name: &str) -> Option<&str> {
719
+ DomArena::attr(self, id, name)
720
+ }
721
+ fn parent(&self, id: u32) -> Option<u32> {
722
+ self.node(id)?.parent
723
+ }
724
+ fn children(&self, id: u32) -> &[u32] {
725
+ self.node(id).map_or(&[], |node| node.children.as_slice())
726
+ }
727
+ fn text(&self, id: u32) -> String {
728
+ DomArena::text(self, &[id])
729
+ }
730
+ fn xml_mode(&self) -> bool {
731
+ DomArena::xml_mode(self)
732
+ }
733
+ fn lower_case_tags(&self) -> bool {
734
+ self.options
735
+ .get("lowerCaseTags")
736
+ .and_then(Value::as_bool)
737
+ .unwrap_or(!self.xml_mode())
738
+ }
739
+ fn lower_case_attribute_names(&self) -> bool {
740
+ self.options
741
+ .get("lowerCaseAttributeNames")
742
+ .and_then(Value::as_bool)
743
+ .unwrap_or(!self.xml_mode())
744
+ }
745
+ fn custom_pseudo(&self, id: u32, name: &str, arg: Option<&str>) -> Option<bool> {
746
+ let key = format!("{}({})", name, arg.unwrap_or(""));
747
+ self.options
748
+ .get("__pseudoMatches")?
749
+ .get(&key)?
750
+ .as_array()
751
+ .map(|ids| ids.iter().any(|value| value.as_u64() == Some(id as u64)))
752
+ }
753
+ fn pseudo_alias(&self, name: &str) -> Option<&str> {
754
+ self.options.get("pseudos")?.get(name)?.as_str()
755
+ }
756
+ }
757
+
758
+ pub(crate) fn value_ids(value: &Value) -> Vec<u32> {
759
+ value
760
+ .as_array()
761
+ .map(|ids| {
762
+ ids.iter()
763
+ .filter_map(|id| id.as_u64().map(|id| id as u32))
764
+ .collect()
765
+ })
766
+ .unwrap_or_default()
767
+ }
768
+ pub(crate) fn xml_mode(options: &Value) -> bool {
769
+ options
770
+ .get("xmlMode")
771
+ .and_then(Value::as_bool)
772
+ .unwrap_or_else(|| {
773
+ options
774
+ .get("xml")
775
+ .is_some_and(|xml| xml.is_object() || xml == &Value::Bool(true))
776
+ })
777
+ }
778
+ pub(crate) fn normalized_options(mut options: Value) -> Value {
779
+ if !options.is_object() {
780
+ options = json!({});
781
+ }
782
+ if let Some(xml) = options.get("xml").and_then(Value::as_object).cloned() {
783
+ options["xmlMode"] = Value::Bool(true);
784
+ options["_useHtmlParser2"] = Value::Bool(true);
785
+ for (key, value) in xml {
786
+ options[&key] = value;
787
+ }
788
+ } else if options.get("xml") == Some(&Value::Bool(true)) {
789
+ options["xmlMode"] = Value::Bool(true);
790
+ options["_useHtmlParser2"] = Value::Bool(true);
791
+ }
792
+ options
793
+ }
794
+ fn escape(value: &str, attribute: bool, xml: bool, utf8: bool, output: &mut String) {
795
+ if xml && !utf8 && crate::codec::has_encoding(value) {
796
+ use std::fmt::Write;
797
+ let units = crate::codec::to_units(value);
798
+ let mut index = 0;
799
+ while index < units.len() {
800
+ let unit = units[index];
801
+ let named = match unit {
802
+ 34 => Some("&quot;"),
803
+ 38 => Some("&amp;"),
804
+ 39 => Some("&apos;"),
805
+ 60 => Some("&lt;"),
806
+ 62 => Some("&gt;"),
807
+ _ => None,
808
+ };
809
+ if let Some(named) = named {
810
+ output.push_str(named);
811
+ index += 1;
812
+ continue;
813
+ }
814
+ if unit >= 0x80 || unit == 0x24 {
815
+ let high = (0xd800..=0xdbff).contains(&unit);
816
+ let code = if high
817
+ && units
818
+ .get(index + 1)
819
+ .is_some_and(|unit| (0xdc00..=0xdfff).contains(unit))
820
+ {
821
+ 0x10000 + ((unit as u32 - 0xd800) << 10) + (units[index + 1] as u32 - 0xdc00)
822
+ } else {
823
+ unit as u32
824
+ };
825
+ let _ = write!(output, "&#x{code:x};");
826
+ // entities.encodeXML advances over a following code unit for
827
+ // every high surrogate, including a lone high surrogate.
828
+ index += if high { 2 } else { 1 };
829
+ } else {
830
+ output.push(char::from_u32(unit as u32).unwrap());
831
+ index += 1;
832
+ }
833
+ }
834
+ return;
835
+ }
836
+ for ch in value.chars() {
837
+ match ch {
838
+ '&' => output.push_str("&amp;"),
839
+ '"' if attribute || xml => output.push_str("&quot;"),
840
+ '\'' if xml => output.push_str("&apos;"),
841
+ '<' if !attribute || xml => output.push_str("&lt;"),
842
+ '>' if !attribute || xml => output.push_str("&gt;"),
843
+ '\u{a0}' if !xml => output.push_str("&nbsp;"),
844
+ ch if xml && !utf8 && (ch > '\u{7f}' || ch == '$') => {
845
+ use std::fmt::Write;
846
+ let _ = write!(output, "&#x{:x};", ch as u32);
847
+ }
848
+ ch => output.push(ch),
849
+ }
850
+ }
851
+ }
852
+
853
+ pub(crate) fn parse_xml(source: &str, options: Value) -> DomArena {
854
+ let mut arena = DomArena::from_records(vec![DomNode::new("root")], options);
855
+ let mut parents = vec![0u32];
856
+ parse_xml_segment(source, &mut arena, &mut parents, 0);
857
+ arena
858
+ }
859
+
860
+ fn parse_xml_segment(source: &str, arena: &mut DomArena, parents: &mut Vec<u32>, base: usize) {
861
+ let xml = arena.xml_mode();
862
+ let indices = arena.options.get("withStartIndices") == Some(&Value::Bool(true))
863
+ || arena.options.get("withEndIndices") == Some(&Value::Bool(true));
864
+ let mut positions = if indices {
865
+ vec![0usize; source.len() + 1]
866
+ } else {
867
+ Vec::new()
868
+ };
869
+ let mut utf16 = 0;
870
+ let codec = arena.options.get("__utf16Codec") == Some(&Value::Bool(true));
871
+ let mut escaped = false;
872
+ if indices {
873
+ for (at, ch) in source.char_indices() {
874
+ for position in &mut positions[at..at + ch.len_utf8()] {
875
+ *position = utf16;
876
+ }
877
+ if codec && ch == crate::codec::ESCAPE && !escaped {
878
+ escaped = true;
879
+ } else {
880
+ utf16 += ch.len_utf16();
881
+ escaped = false;
882
+ }
883
+ positions[at + ch.len_utf8()] = utf16;
884
+ }
885
+ }
886
+ let source_position = |index: usize| {
887
+ base + if indices {
888
+ positions[index.min(source.len())]
889
+ } else {
890
+ index
891
+ }
892
+ };
893
+ macro_rules! append_parsed {
894
+ ($arena:expr,$parent:expr,$node:expr,$start:expr,$end:expr $(,)?) => {
895
+ append_parsed(
896
+ $arena,
897
+ $parent,
898
+ $node,
899
+ source_position($start),
900
+ source_position(($end).saturating_add(1)).saturating_sub(1),
901
+ )
902
+ };
903
+ }
904
+ let mut index = 0;
905
+ let bytes = source.as_bytes();
906
+ let decode = arena.options.get("decodeEntities") != Some(&Value::Bool(false));
907
+ while index < bytes.len() {
908
+ let start = index;
909
+ if !xml {
910
+ if let Some(name) = parents
911
+ .last()
912
+ .and_then(|id| arena.node(*id))
913
+ .and_then(|node| node.name.as_deref())
914
+ .filter(|name| matches!(*name, "script" | "style" | "xmp" | "textarea" | "title"))
915
+ .map(str::to_owned)
916
+ {
917
+ let closing = format!("</{name}");
918
+ let end = source.as_bytes()[index..]
919
+ .windows(closing.len())
920
+ .enumerate()
921
+ .find(|(offset, candidate)| {
922
+ candidate.eq_ignore_ascii_case(closing.as_bytes())
923
+ && bytes
924
+ .get(index + offset + closing.len())
925
+ .is_none_or(|ch| *ch == b'>' || ch.is_ascii_whitespace())
926
+ })
927
+ .map_or(bytes.len(), |(offset, _)| index + offset);
928
+ if end > index {
929
+ let text = &source[index..end];
930
+ let mut node = DomNode::new("text");
931
+ node.data = Some(if decode && name == "title" {
932
+ decode_entities_mode(
933
+ text,
934
+ false,
935
+ false,
936
+ arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
937
+ )
938
+ } else {
939
+ text.into()
940
+ });
941
+ append_parsed!(
942
+ arena,
943
+ *parents.last().unwrap(),
944
+ node,
945
+ index,
946
+ end.saturating_sub(1),
947
+ );
948
+ index = end;
949
+ continue;
950
+ }
951
+ }
952
+ if bytes[index] == b'<'
953
+ && bytes.get(index + 1).is_some_and(|ch| {
954
+ !ch.is_ascii_alphabetic() && !matches!(*ch, b'!' | b'?' | b'/')
955
+ })
956
+ {
957
+ let mut node = DomNode::new("text");
958
+ node.data = Some("<".into());
959
+ append_parsed!(arena, *parents.last().unwrap(), node, index, index);
960
+ index += 1;
961
+ continue;
962
+ }
963
+ }
964
+ if bytes[index] != b'<' {
965
+ index = source[index..]
966
+ .find('<')
967
+ .map_or(bytes.len(), |offset| index + offset);
968
+ let text = &source[start..index];
969
+ let mut node = DomNode::new("text");
970
+ node.data = Some(if decode {
971
+ decode_entities_mode(
972
+ text,
973
+ xml,
974
+ false,
975
+ arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
976
+ )
977
+ } else {
978
+ text.into()
979
+ });
980
+ append_parsed!(
981
+ arena,
982
+ *parents.last().unwrap(),
983
+ node,
984
+ start,
985
+ index.saturating_sub(1),
986
+ );
987
+ continue;
988
+ }
989
+ if source[index..].starts_with("<!--") {
990
+ let end = source[index + 4..]
991
+ .find("-->")
992
+ .map(|offset| index + 4 + offset);
993
+ let data_end = end.unwrap_or(bytes.len());
994
+ index = end.map_or(bytes.len(), |end| end + 3);
995
+ let mut node = DomNode::new("comment");
996
+ node.data = Some(source[start + 4..data_end].into());
997
+ append_parsed!(
998
+ arena,
999
+ *parents.last().unwrap(),
1000
+ node,
1001
+ start,
1002
+ index.saturating_sub(1),
1003
+ );
1004
+ continue;
1005
+ }
1006
+ if source[index..].starts_with("<![CDATA[") {
1007
+ let end = source[index + 9..]
1008
+ .find("]]>")
1009
+ .map(|offset| index + 9 + offset);
1010
+ let data_end = end.unwrap_or(bytes.len());
1011
+ index = end.map_or(bytes.len(), |end| end + 3);
1012
+ if !xml && arena.options.get("recognizeCDATA") != Some(&Value::Bool(true)) {
1013
+ let mut node = DomNode::new("comment");
1014
+ node.data = Some(format!("[CDATA[{}]]", &source[start + 9..data_end]));
1015
+ append_parsed!(
1016
+ arena,
1017
+ *parents.last().unwrap(),
1018
+ node,
1019
+ start,
1020
+ index.saturating_sub(1)
1021
+ );
1022
+ continue;
1023
+ }
1024
+ let id = append_parsed!(
1025
+ arena,
1026
+ *parents.last().unwrap(),
1027
+ DomNode::new("cdata"),
1028
+ start,
1029
+ index.saturating_sub(1),
1030
+ );
1031
+ let mut text = DomNode::new("text");
1032
+ text.data = Some(source[start + 9..data_end].into());
1033
+ append_parsed!(arena, id, text, start + 9, data_end.saturating_sub(1));
1034
+ continue;
1035
+ }
1036
+ let mut cursor = index + 1;
1037
+ let mut quote = None;
1038
+ while cursor < bytes.len() {
1039
+ let ch = bytes[cursor];
1040
+ if let Some(q) = quote {
1041
+ if ch == q {
1042
+ quote = None;
1043
+ }
1044
+ } else if ch == b'\'' || ch == b'"' {
1045
+ quote = Some(ch);
1046
+ } else if ch == b'>' {
1047
+ break;
1048
+ }
1049
+ cursor += 1;
1050
+ }
1051
+ if cursor >= bytes.len() {
1052
+ let mut text = DomNode::new("text");
1053
+ text.data = Some(source[index..].into());
1054
+ append_parsed!(
1055
+ arena,
1056
+ *parents.last().unwrap(),
1057
+ text,
1058
+ start,
1059
+ bytes.len().saturating_sub(1),
1060
+ );
1061
+ break;
1062
+ }
1063
+ index = cursor + 1;
1064
+ let token = source[start + 1..cursor].trim();
1065
+ if token.starts_with('!') || token.starts_with('?') {
1066
+ let mut node = DomNode::new("directive");
1067
+ node.name = Some(token.split_whitespace().next().unwrap_or("").into());
1068
+ node.data = Some(token.into());
1069
+ append_parsed!(arena, *parents.last().unwrap(), node, start, cursor);
1070
+ continue;
1071
+ }
1072
+ if let Some(close) = token.strip_prefix('/') {
1073
+ let close = close.split_whitespace().next().unwrap_or("");
1074
+ if let Some(position) = parents.iter().rposition(|id| {
1075
+ arena.nodes[*id as usize]
1076
+ .name
1077
+ .as_deref()
1078
+ .is_some_and(|name| {
1079
+ if xml {
1080
+ name == close
1081
+ } else {
1082
+ name.eq_ignore_ascii_case(close)
1083
+ }
1084
+ })
1085
+ }) {
1086
+ for id in parents.drain(position..) {
1087
+ if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
1088
+ arena.nodes[id as usize]
1089
+ .extras
1090
+ .insert("endIndex".into(), json!(source_position(cursor)));
1091
+ }
1092
+ }
1093
+ }
1094
+ continue;
1095
+ }
1096
+ let slash = token.ends_with('/');
1097
+ let content = token.strip_suffix('/').unwrap_or(token);
1098
+ let name_end = content.find(char::is_whitespace).unwrap_or(content.len());
1099
+ let name = &content[..name_end];
1100
+ if name.is_empty() {
1101
+ continue;
1102
+ }
1103
+ let lower = arena
1104
+ .options
1105
+ .get("lowerCaseTags")
1106
+ .and_then(Value::as_bool)
1107
+ .unwrap_or(!xml);
1108
+ let name = if lower {
1109
+ name.to_ascii_lowercase()
1110
+ } else {
1111
+ name.into()
1112
+ };
1113
+ if !xml {
1114
+ while parents
1115
+ .last()
1116
+ .and_then(|id| arena.node(*id))
1117
+ .and_then(|node| node.name.as_deref())
1118
+ .is_some_and(|open| implied_close(&name, open))
1119
+ {
1120
+ let id = parents.pop().unwrap();
1121
+ if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
1122
+ arena.nodes[id as usize].extras.insert(
1123
+ "endIndex".into(),
1124
+ json!(source_position(start.saturating_sub(1))),
1125
+ );
1126
+ }
1127
+ }
1128
+ }
1129
+ let foreign = matches!(name.as_str(), "svg" | "math")
1130
+ || parents
1131
+ .iter()
1132
+ .rev()
1133
+ .filter_map(|id| arena.node(*id))
1134
+ .filter_map(|node| node.name.as_deref())
1135
+ .find_map(|name| {
1136
+ if matches!(
1137
+ name,
1138
+ "mi" | "mo"
1139
+ | "mn"
1140
+ | "ms"
1141
+ | "mtext"
1142
+ | "annotation-xml"
1143
+ | "foreignobject"
1144
+ | "desc"
1145
+ | "title"
1146
+ ) {
1147
+ Some(false)
1148
+ } else if matches!(name, "svg" | "math") {
1149
+ Some(true)
1150
+ } else {
1151
+ None
1152
+ }
1153
+ })
1154
+ .unwrap_or(false);
1155
+ let self_close = slash
1156
+ && (xml
1157
+ || foreign
1158
+ || arena.options.get("recognizeSelfClosing") == Some(&Value::Bool(true)));
1159
+ let mut node = DomNode::new(if !xml && name == "script" {
1160
+ "script"
1161
+ } else if !xml && name == "style" {
1162
+ "style"
1163
+ } else {
1164
+ "tag"
1165
+ });
1166
+ node.name = Some(name.clone());
1167
+ node.attribs = parse_attributes(
1168
+ &content[name_end..],
1169
+ decode,
1170
+ arena
1171
+ .options
1172
+ .get("lowerCaseAttributeNames")
1173
+ .and_then(Value::as_bool)
1174
+ .unwrap_or(!xml),
1175
+ xml,
1176
+ arena.options.get("__utf16Codec") == Some(&Value::Bool(true)),
1177
+ );
1178
+ let id = append_parsed!(arena, *parents.last().unwrap(), node, start, cursor);
1179
+ let void = !xml && html_parser_void(&name);
1180
+ if !self_close && !void {
1181
+ parents.push(id);
1182
+ } else if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
1183
+ arena.nodes[id as usize]
1184
+ .extras
1185
+ .insert("endIndex".into(), json!(source_position(cursor)));
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ /// Incremental HTML-parser2/XML state; only incomplete markup/entity tokens are
1191
+ /// buffered, while completed input is discarded after native tree construction.
1192
+ pub(crate) struct XmlStream {
1193
+ pub arena: DomArena,
1194
+ parents: Vec<u32>,
1195
+ pending: String,
1196
+ offset: usize,
1197
+ }
1198
+ impl XmlStream {
1199
+ pub(crate) fn new(options: Value) -> Self {
1200
+ Self {
1201
+ arena: DomArena::from_records(vec![DomNode::new("root")], options),
1202
+ parents: vec![0],
1203
+ pending: String::new(),
1204
+ offset: 0,
1205
+ }
1206
+ }
1207
+ pub(crate) fn feed(&mut self, chunk: &str, finish: bool) {
1208
+ self.pending.push_str(chunk);
1209
+ let mut consumed = 0;
1210
+ loop {
1211
+ let input = &self.pending[consumed..];
1212
+ let mut length = if finish {
1213
+ input.len()
1214
+ } else {
1215
+ self.complete_prefix(input)
1216
+ };
1217
+ if !finish
1218
+ && self.arena.options.get("__utf16Codec") == Some(&Value::Bool(true))
1219
+ && input[..length]
1220
+ .chars()
1221
+ .rev()
1222
+ .take_while(|ch| *ch == crate::codec::ESCAPE)
1223
+ .count()
1224
+ % 2
1225
+ == 1
1226
+ {
1227
+ length -= crate::codec::ESCAPE.len_utf8();
1228
+ }
1229
+ if length == 0 {
1230
+ break;
1231
+ }
1232
+ let segment = &input[..length];
1233
+ parse_xml_segment(segment, &mut self.arena, &mut self.parents, self.offset);
1234
+ self.offset += if self.arena.options.get("__utf16Codec") == Some(&Value::Bool(true)) {
1235
+ crate::codec::logical_len(segment)
1236
+ } else {
1237
+ segment.encode_utf16().count()
1238
+ };
1239
+ consumed += length;
1240
+ if consumed == self.pending.len() || finish {
1241
+ break;
1242
+ }
1243
+ }
1244
+ self.pending.drain(..consumed);
1245
+ }
1246
+ fn complete_prefix(&self, input: &str) -> usize {
1247
+ let bytes = input.as_bytes();
1248
+ if input.is_empty() {
1249
+ return 0;
1250
+ }
1251
+ if !self.arena.xml_mode() {
1252
+ if let Some(name) = self
1253
+ .parents
1254
+ .last()
1255
+ .and_then(|id| self.arena.node(*id))
1256
+ .and_then(|node| node.name.as_deref())
1257
+ .filter(|name| matches!(*name, "script" | "style" | "xmp" | "textarea" | "title"))
1258
+ {
1259
+ let close = format!("</{name}");
1260
+ let closing = bytes
1261
+ .windows(close.len())
1262
+ .enumerate()
1263
+ .find(|(at, value)| {
1264
+ value.eq_ignore_ascii_case(close.as_bytes())
1265
+ && bytes
1266
+ .get(at + close.len())
1267
+ .is_none_or(|ch| *ch == b'>' || ch.is_ascii_whitespace())
1268
+ })
1269
+ .map(|(at, _)| at);
1270
+ match closing {
1271
+ Some(0) if bytes.len() == close.len() => return 0,
1272
+ Some(0) => {}
1273
+ Some(index) => return safe_entity_prefix(input, index, name == "title"),
1274
+ None => {
1275
+ let mut end = input.len().saturating_sub(close.len() + 1);
1276
+ while !input.is_char_boundary(end) {
1277
+ end -= 1;
1278
+ }
1279
+ return safe_entity_prefix(input, end, name == "title");
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+ if bytes[0] != b'<' {
1285
+ let end = input.find('<').unwrap_or(input.len());
1286
+ return safe_entity_prefix(
1287
+ input,
1288
+ end,
1289
+ self.arena.options.get("decodeEntities") != Some(&Value::Bool(false)),
1290
+ );
1291
+ }
1292
+ if input.starts_with("<!--") {
1293
+ return input.find("-->").map_or(0, |index| index + 3);
1294
+ }
1295
+ if input.starts_with("<![CDATA[") {
1296
+ return input.find("]]>").map_or(0, |index| index + 3);
1297
+ }
1298
+ let mut quote = None;
1299
+ for (index, ch) in bytes.iter().copied().enumerate().skip(1) {
1300
+ if let Some(q) = quote {
1301
+ if ch == q {
1302
+ quote = None;
1303
+ }
1304
+ } else if ch == b'\'' || ch == b'"' {
1305
+ quote = Some(ch);
1306
+ } else if ch == b'>' {
1307
+ return index + 1;
1308
+ }
1309
+ }
1310
+ 0
1311
+ }
1312
+ }
1313
+ fn safe_entity_prefix(source: &str, end: usize, decode: bool) -> usize {
1314
+ if decode {
1315
+ if let Some(start) = source[..end].rfind('&') {
1316
+ let tail = &source[start + 1..end];
1317
+ if tail.len() < 64
1318
+ && tail
1319
+ .chars()
1320
+ .all(|ch| ch.is_ascii_alphanumeric() || ch == '#')
1321
+ {
1322
+ return start;
1323
+ }
1324
+ }
1325
+ }
1326
+ end
1327
+ }
1328
+
1329
+ pub(crate) fn append_parsed(
1330
+ arena: &mut DomArena,
1331
+ parent: u32,
1332
+ mut node: DomNode,
1333
+ start: usize,
1334
+ end: usize,
1335
+ ) -> u32 {
1336
+ if node.kind == "text" {
1337
+ if let Some(previous) = arena.nodes[parent as usize]
1338
+ .children
1339
+ .last()
1340
+ .copied()
1341
+ .filter(|id| arena.nodes[*id as usize].kind == "text")
1342
+ {
1343
+ arena.nodes[previous as usize]
1344
+ .data
1345
+ .get_or_insert_with(String::new)
1346
+ .push_str(node.data.as_deref().unwrap_or(""));
1347
+ if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
1348
+ arena.nodes[previous as usize]
1349
+ .extras
1350
+ .insert("endIndex".into(), json!(end));
1351
+ }
1352
+ return previous;
1353
+ }
1354
+ }
1355
+ if arena.options.get("withStartIndices") == Some(&Value::Bool(true)) {
1356
+ node.extras.insert("startIndex".into(), json!(start));
1357
+ }
1358
+ if arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
1359
+ node.extras.insert("endIndex".into(), json!(end));
1360
+ }
1361
+ let previous = arena.nodes[parent as usize].children.last().copied();
1362
+ node.parent = Some(parent);
1363
+ node.prev = previous;
1364
+ let id = arena.push(node);
1365
+ if let Some(previous) = previous {
1366
+ arena.nodes[previous as usize].next = Some(id);
1367
+ }
1368
+ arena.nodes[parent as usize].children.push(id);
1369
+ id
1370
+ }
1371
+
1372
+ pub(crate) fn html_parser_void(name: &str) -> bool {
1373
+ (super::void_name(name) && name != "bgsound") || matches!(name, "command" | "isindex")
1374
+ }
1375
+ pub(crate) fn implied_close(name: &str, open: &str) -> bool {
1376
+ match name {
1377
+ "tr" => matches!(open, "tr" | "th" | "td"),
1378
+ "th" => open == "th",
1379
+ "td" => matches!(open, "thead" | "th" | "td"),
1380
+ "body" => matches!(open, "head" | "link" | "script"),
1381
+ "li" => open == "li",
1382
+ "option" => open == "option",
1383
+ "optgroup" => matches!(open, "option" | "optgroup"),
1384
+ "select" | "input" | "output" | "button" | "datalist" | "textarea" => matches!(
1385
+ open,
1386
+ "input" | "option" | "optgroup" | "select" | "button" | "datalist" | "textarea"
1387
+ ),
1388
+ "dd" | "dt" => matches!(open, "dd" | "dt"),
1389
+ "rt" | "rp" => matches!(open, "rt" | "rp"),
1390
+ "tbody" | "tfoot" => matches!(open, "thead" | "tbody"),
1391
+ "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "address" | "article" | "aside"
1392
+ | "blockquote" | "details" | "div" | "dl" | "fieldset" | "figcaption" | "figure"
1393
+ | "footer" | "form" | "header" | "hr" | "main" | "nav" | "ol" | "pre" | "section"
1394
+ | "table" | "ul" => open == "p",
1395
+ _ => false,
1396
+ }
1397
+ }
1398
+ fn parse_attributes(
1399
+ source: &str,
1400
+ decode: bool,
1401
+ lower: bool,
1402
+ xml: bool,
1403
+ codec: bool,
1404
+ ) -> Map<String, Value> {
1405
+ let bytes = source.as_bytes();
1406
+ let mut index = 0;
1407
+ let mut attrs = Map::new();
1408
+ while index < bytes.len() {
1409
+ while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1410
+ index += 1;
1411
+ }
1412
+ let start = index;
1413
+ while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'=' {
1414
+ index += 1;
1415
+ }
1416
+ if start == index {
1417
+ index += 1;
1418
+ continue;
1419
+ }
1420
+ let name = &source[start..index];
1421
+ while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1422
+ index += 1;
1423
+ }
1424
+ let mut value = "";
1425
+ if bytes.get(index) == Some(&b'=') {
1426
+ index += 1;
1427
+ while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1428
+ index += 1;
1429
+ }
1430
+ if matches!(bytes.get(index), Some(b'\'' | b'"')) {
1431
+ let quote = bytes[index];
1432
+ index += 1;
1433
+ let start = index;
1434
+ while index < bytes.len() && bytes[index] != quote {
1435
+ index += 1;
1436
+ }
1437
+ value = &source[start..index];
1438
+ index += (index < bytes.len()) as usize;
1439
+ } else {
1440
+ let start = index;
1441
+ while index < bytes.len() && !bytes[index].is_ascii_whitespace() {
1442
+ index += 1;
1443
+ }
1444
+ value = &source[start..index];
1445
+ }
1446
+ }
1447
+ let name = if lower {
1448
+ name.to_ascii_lowercase()
1449
+ } else {
1450
+ name.into()
1451
+ };
1452
+ attrs.entry(name).or_insert_with(|| {
1453
+ Value::String(if decode {
1454
+ decode_entities_mode(value, xml, true, codec)
1455
+ } else {
1456
+ value.into()
1457
+ })
1458
+ });
1459
+ }
1460
+ attrs
1461
+ }
1462
+ pub(crate) fn decode_entities_mode(
1463
+ source: &str,
1464
+ xml: bool,
1465
+ attribute: bool,
1466
+ codec: bool,
1467
+ ) -> String {
1468
+ let scalar_string = |scalar: char| {
1469
+ if codec {
1470
+ crate::codec::encode_scalar(scalar)
1471
+ } else {
1472
+ scalar.to_string()
1473
+ }
1474
+ };
1475
+ let bytes = source.as_bytes();
1476
+ let mut output = String::with_capacity(source.len());
1477
+ let mut cursor = 0;
1478
+ while let Some(relative) = source[cursor..].find('&') {
1479
+ let amp = cursor + relative;
1480
+ output.push_str(&source[cursor..amp]);
1481
+ let mut end = amp + 1;
1482
+ let mut decoded = None;
1483
+ if bytes.get(end) == Some(&b'#') {
1484
+ end += 1;
1485
+ let hexadecimal = matches!(bytes.get(end), Some(b'x' | b'X'));
1486
+ if hexadecimal {
1487
+ end += 1;
1488
+ }
1489
+ let digits = end;
1490
+ let mut number = 0u32;
1491
+ while let Some(digit) = bytes
1492
+ .get(end)
1493
+ .and_then(|value| (*value as char).to_digit(if hexadecimal { 16 } else { 10 }))
1494
+ {
1495
+ number = number
1496
+ .saturating_mul(if hexadecimal { 16 } else { 10 })
1497
+ .saturating_add(digit);
1498
+ end += 1;
1499
+ }
1500
+ let semicolon = bytes.get(end) == Some(&b';');
1501
+ if digits != end && (!xml || semicolon) {
1502
+ if semicolon {
1503
+ end += 1;
1504
+ }
1505
+ let scalar = match number {
1506
+ 0 | 0xd800..=0xdfff | 0x110000..=u32::MAX => '\u{fffd}',
1507
+ 0x80..=0x9f => html5ever::data::C1_REPLACEMENTS[(number - 0x80) as usize]
1508
+ .unwrap_or(char::from_u32(number).unwrap()),
1509
+ _ => char::from_u32(number).unwrap(),
1510
+ };
1511
+ decoded = Some(scalar_string(scalar));
1512
+ }
1513
+ } else {
1514
+ while bytes.get(end).is_some_and(u8::is_ascii_alphanumeric) {
1515
+ end += 1;
1516
+ }
1517
+ if bytes.get(end) == Some(&b';') {
1518
+ end += 1;
1519
+ }
1520
+ if xml {
1521
+ let scalar = match &source[amp + 1..end] {
1522
+ "amp;" => Some('&'),
1523
+ "lt;" => Some('<'),
1524
+ "gt;" => Some('>'),
1525
+ "quot;" => Some('"'),
1526
+ "apos;" => Some('\''),
1527
+ _ => None,
1528
+ };
1529
+ decoded = scalar.map(scalar_string);
1530
+ } else {
1531
+ for candidate in (amp + 2..=end).rev() {
1532
+ if let Some(&(first, second)) =
1533
+ html5ever::data::NAMED_ENTITIES.get(&source[amp + 1..candidate])
1534
+ {
1535
+ if first == 0 {
1536
+ continue;
1537
+ }
1538
+ if bytes[candidate - 1] != b';'
1539
+ && attribute
1540
+ && bytes
1541
+ .get(candidate)
1542
+ .is_some_and(|next| next.is_ascii_alphanumeric() || *next == b'=')
1543
+ {
1544
+ break;
1545
+ }
1546
+ let mut value = scalar_string(char::from_u32(first).unwrap());
1547
+ if second != 0 {
1548
+ value.push_str(&scalar_string(char::from_u32(second).unwrap()));
1549
+ }
1550
+ decoded = Some(value);
1551
+ end = candidate;
1552
+ break;
1553
+ }
1554
+ }
1555
+ }
1556
+ }
1557
+ if let Some(value) = decoded {
1558
+ output.push_str(&value);
1559
+ cursor = end;
1560
+ } else {
1561
+ output.push('&');
1562
+ cursor = amp + 1;
1563
+ }
1564
+ }
1565
+ output.push_str(&source[cursor..]);
1566
+ output
1567
+ }
1568
+
1569
+ pub(crate) fn primitive(arena: &mut DomArena, command: &str, args: &Value) -> Result<Value> {
1570
+ let ids = value_ids(&args["ids"]);
1571
+ let id = args["id"].as_u64().unwrap_or(0) as u32;
1572
+ Ok(match command {
1573
+ "node" => arena
1574
+ .node(id)
1575
+ .map(|node| serde_json::to_value(node).unwrap())
1576
+ .unwrap_or(Value::Null),
1577
+ "createNode" => {
1578
+ let mut fields = args.clone();
1579
+ if let Some(fields) = fields.as_object_mut() {
1580
+ fields.remove("options");
1581
+ }
1582
+ let mut node: DomNode = serde_json::from_value(fields)
1583
+ .map_err(|error| Error::new(Status::InvalidArg, error.to_string()))?;
1584
+ node.children.clear();
1585
+ node.parent = None;
1586
+ node.prev = None;
1587
+ node.next = None;
1588
+ json!(arena.push(node))
1589
+ }
1590
+ "setNode" => {
1591
+ arena.set_node(id, args["field"].as_str().unwrap_or(""), &args["value"])?;
1592
+ Value::Null
1593
+ }
1594
+ "attrs" => {
1595
+ if let Some(value) = args.get("value") {
1596
+ arena.set_node(id, "attribs", value)?;
1597
+ Value::Null
1598
+ } else {
1599
+ Value::Object(
1600
+ arena
1601
+ .node(id)
1602
+ .map(|node| node.attribs.clone())
1603
+ .unwrap_or_default(),
1604
+ )
1605
+ }
1606
+ }
1607
+ "attr" => {
1608
+ let name = args["name"].as_str().unwrap_or("");
1609
+ if let Some(value) = args.get("value") {
1610
+ let value = value
1611
+ .as_str()
1612
+ .map(str::to_owned)
1613
+ .or_else(|| (!value.is_null()).then(|| value.to_string()));
1614
+ arena.set_attr(&ids, name, value.as_deref());
1615
+ Value::Null
1616
+ } else {
1617
+ ids.first()
1618
+ .and_then(|id| arena.attr(*id, name))
1619
+ .map(|value| json!(value))
1620
+ .unwrap_or(Value::Null)
1621
+ }
1622
+ }
1623
+ "text" => {
1624
+ if let Some(value) = args.get("value") {
1625
+ arena.set_text(&ids, value.as_str().unwrap_or(""));
1626
+ Value::Null
1627
+ } else {
1628
+ json!(arena.text(&ids))
1629
+ }
1630
+ }
1631
+ "html" => {
1632
+ if let Some(value) = args.get("value") {
1633
+ arena.set_html(&ids, value.as_str().unwrap_or(""))?;
1634
+ Value::Null
1635
+ } else {
1636
+ ids.first()
1637
+ .map(|id| json!(arena.inner_html_mut(*id)))
1638
+ .unwrap_or(Value::Null)
1639
+ }
1640
+ }
1641
+ "outerHtml" | "render" => {
1642
+ let mut options = arena.options.clone();
1643
+ if let Some(overrides) = args.get("options").and_then(Value::as_object) {
1644
+ for (key, value) in overrides {
1645
+ options[key] = value.clone();
1646
+ }
1647
+ }
1648
+ json!(arena.serialize(&ids, &normalized_options(options)))
1649
+ }
1650
+ "traverse" => json!(arena.traverse(&ids, args["kind"].as_str().unwrap_or("children"))),
1651
+ "select" | "matches" => {
1652
+ let previous = arena
1653
+ .options
1654
+ .as_object_mut()
1655
+ .and_then(|options| options.remove("__pseudoMatches"));
1656
+ if let Some(matches) = args.get("pseudoMatches") {
1657
+ arena.options["__pseudoMatches"] = matches.clone();
1658
+ }
1659
+ let result = if command == "select" {
1660
+ super::selector::select(
1661
+ arena,
1662
+ args["selector"].as_str().unwrap_or(""),
1663
+ args.get("contexts").map(value_ids).as_deref(),
1664
+ )
1665
+ } else {
1666
+ super::selector::filter(arena, args["selector"].as_str().unwrap_or(""), &ids)
1667
+ };
1668
+ arena
1669
+ .options
1670
+ .as_object_mut()
1671
+ .unwrap()
1672
+ .remove("__pseudoMatches");
1673
+ if let Some(previous) = previous {
1674
+ arena.options["__pseudoMatches"] = previous;
1675
+ }
1676
+ json!(result?)
1677
+ }
1678
+ "create" => json!(arena.create_fragment(
1679
+ args["html"].as_str().unwrap_or(""),
1680
+ args["contextId"].as_u64().map(|id| id as u32)
1681
+ )?),
1682
+ "import" => {
1683
+ let records: Vec<DomNode> = serde_json::from_value(args["nodes"].clone())
1684
+ .map_err(|error| Error::new(Status::InvalidArg, error.to_string()))?;
1685
+ json!(arena.import(&records, &value_ids(&args["roots"])))
1686
+ }
1687
+ "clone" => json!(arena.clone_ids(&ids, args["deep"].as_bool().unwrap_or(true))),
1688
+ "insert" => json!(arena.insert(
1689
+ &value_ids(&args["targetIds"]),
1690
+ &value_ids(&args["nodeIds"]),
1691
+ args["position"].as_str().unwrap_or("append")
1692
+ )),
1693
+ "remove" => {
1694
+ if args["keepChildren"].as_bool() == Some(true) {
1695
+ for id in &ids {
1696
+ let children = arena
1697
+ .node(*id)
1698
+ .map(|node| node.children.clone())
1699
+ .unwrap_or_default();
1700
+ arena.insert(&[*id], &children, "before");
1701
+ arena.detach(*id);
1702
+ }
1703
+ } else {
1704
+ arena.detach_many(&ids);
1705
+ }
1706
+ Value::Null
1707
+ }
1708
+ "uniqueSort" => {
1709
+ let mut ids = ids;
1710
+ let mut unique = HashSet::new();
1711
+ ids.retain(|id| unique.insert(*id));
1712
+ let ancestry = |id: u32| {
1713
+ let mut path = vec![id];
1714
+ let mut seen = HashSet::from([id]);
1715
+ let mut parent = arena.node(id).and_then(|node| node.parent);
1716
+ while let Some(id) = parent {
1717
+ if !seen.insert(id) {
1718
+ break;
1719
+ }
1720
+ path.push(id);
1721
+ parent = arena.node(id).and_then(|node| node.parent);
1722
+ }
1723
+ path.reverse();
1724
+ path
1725
+ };
1726
+ let paths: HashMap<_, _> = ids.iter().map(|id| (*id, ancestry(*id))).collect();
1727
+ ids.sort_by(|a, b| {
1728
+ use std::cmp::Ordering;
1729
+ let (a, b) = (&paths[a], &paths[b]);
1730
+ if a.first() != b.first() {
1731
+ return Ordering::Equal;
1732
+ }
1733
+ for index in 0..a.len().min(b.len()) {
1734
+ if a[index] != b[index] {
1735
+ let siblings = arena
1736
+ .node(a[index - 1])
1737
+ .map(|node| node.children.as_slice())
1738
+ .unwrap_or(&[]);
1739
+ return siblings
1740
+ .iter()
1741
+ .position(|id| *id == a[index])
1742
+ .cmp(&siblings.iter().position(|id| *id == b[index]));
1743
+ }
1744
+ }
1745
+ a.len().cmp(&b.len())
1746
+ });
1747
+ json!(ids)
1748
+ }
1749
+ "contains" => {
1750
+ let parent = args
1751
+ .get("container")
1752
+ .or_else(|| args.get("parent"))
1753
+ .and_then(Value::as_u64)
1754
+ .unwrap_or(0) as u32;
1755
+ let mut node = args
1756
+ .get("contained")
1757
+ .or_else(|| args.get("child"))
1758
+ .and_then(Value::as_u64)
1759
+ .map(|id| id as u32);
1760
+ let mut found = false;
1761
+ let mut seen = HashSet::new();
1762
+ while let Some(id) = node {
1763
+ if !seen.insert(id) {
1764
+ break;
1765
+ }
1766
+ node = arena.node(id).and_then(|node| node.parent);
1767
+ if node == Some(parent) {
1768
+ found = true;
1769
+ break;
1770
+ }
1771
+ }
1772
+ json!(found)
1773
+ }
1774
+ _ => {
1775
+ return Err(Error::new(
1776
+ Status::InvalidArg,
1777
+ format!("Unknown native DOM command {command}"),
1778
+ ))
1779
+ }
1780
+ })
1781
+ }