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,392 @@
1
+ //! Source token locations for explicitly written HTML elements. Offsets and
2
+ //! columns count UTF-16 code units, matching JavaScript parser conventions.
3
+ use super::arena::DomNode;
4
+ use serde_json::{json, Map, Value};
5
+ use std::collections::{HashMap, VecDeque};
6
+
7
+ #[derive(Clone, Copy)]
8
+ struct Position {
9
+ line: usize,
10
+ column: usize,
11
+ offset: usize,
12
+ }
13
+ struct Element {
14
+ name: String,
15
+ start: usize,
16
+ end: usize,
17
+ start_end: usize,
18
+ end_tag: Option<(usize, usize)>,
19
+ attrs: Vec<(String, usize, usize)>,
20
+ }
21
+
22
+ fn range(positions: &[Position], start: usize, end: usize) -> Value {
23
+ let a = positions[start];
24
+ let b = positions[end];
25
+ json!({"startLine":a.line,"startCol":a.column,"startOffset":a.offset,"endLine":b.line,"endCol":b.column,"endOffset":b.offset})
26
+ }
27
+
28
+ pub(crate) fn annotate(source: &str, nodes: &mut [DomNode], scripting: bool, codec: bool) {
29
+ let mut positions = vec![
30
+ Position {
31
+ line: 1,
32
+ column: 1,
33
+ offset: 0
34
+ };
35
+ source.len() + 1
36
+ ];
37
+ let (mut line, mut column, mut offset) = (1, 1, 0);
38
+ let mut cr = false;
39
+ let mut escaped = false;
40
+ for (index, ch) in source.char_indices() {
41
+ let current = Position {
42
+ line,
43
+ column,
44
+ offset,
45
+ };
46
+ for position in &mut positions[index..index + ch.len_utf8()] {
47
+ *position = current;
48
+ }
49
+ let width = if codec && !escaped && ch == crate::codec::ESCAPE {
50
+ escaped = true;
51
+ 0
52
+ } else {
53
+ escaped = false;
54
+ ch.len_utf16()
55
+ };
56
+ offset += width;
57
+ if ch == '\r' {
58
+ line += 1;
59
+ column = 1;
60
+ cr = true;
61
+ } else if ch == '\n' {
62
+ if !cr {
63
+ line += 1;
64
+ }
65
+ column = 1;
66
+ cr = false;
67
+ } else {
68
+ column += width;
69
+ cr = false;
70
+ }
71
+ positions[index + ch.len_utf8()] = Position {
72
+ line,
73
+ column,
74
+ offset,
75
+ };
76
+ }
77
+ let bytes = source.as_bytes();
78
+ let mut characters: Vec<(String, usize, usize, bool)> = Vec::new();
79
+ let mut comments: VecDeque<(usize, usize)> = VecDeque::new();
80
+ let mut doctypes: VecDeque<(usize, usize)> = VecDeque::new();
81
+ let mut elements: Vec<Element> = Vec::new();
82
+ let mut stack: Vec<usize> = Vec::new();
83
+ let mut index = 0;
84
+ while index < bytes.len() {
85
+ if let Some(open) = stack.last().copied() {
86
+ let name = &elements[open].name;
87
+ let raw = matches!(
88
+ name.as_str(),
89
+ "script"
90
+ | "style"
91
+ | "textarea"
92
+ | "title"
93
+ | "xmp"
94
+ | "iframe"
95
+ | "noembed"
96
+ | "noframes"
97
+ | "plaintext"
98
+ ) || (scripting && name == "noscript");
99
+ if raw {
100
+ let close = format!("</{name}");
101
+ if !source[index..]
102
+ .get(..close.len())
103
+ .is_some_and(|value| value.eq_ignore_ascii_case(&close))
104
+ {
105
+ let mut found = None;
106
+ let mut search = index;
107
+ while let Some(relative) = source[search..].find('<') {
108
+ let candidate = search + relative;
109
+ if source[candidate..]
110
+ .get(..close.len())
111
+ .is_some_and(|value| value.eq_ignore_ascii_case(&close))
112
+ {
113
+ found = Some(candidate);
114
+ break;
115
+ }
116
+ search = candidate + 1;
117
+ }
118
+ if let Some(found) = found {
119
+ characters.push((
120
+ source[index..found].to_owned(),
121
+ index,
122
+ found,
123
+ matches!(name.as_str(), "textarea" | "title"),
124
+ ));
125
+ index = found;
126
+ } else {
127
+ characters.push((
128
+ source[index..].to_owned(),
129
+ index,
130
+ bytes.len(),
131
+ matches!(name.as_str(), "textarea" | "title"),
132
+ ));
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ }
138
+ let Some(relative) = source[index..].find('<') else {
139
+ characters.push((source[index..].to_owned(), index, bytes.len(), true));
140
+ break;
141
+ };
142
+ if relative > 0 {
143
+ characters.push((
144
+ source[index..index + relative].to_owned(),
145
+ index,
146
+ index + relative,
147
+ true,
148
+ ));
149
+ }
150
+ index += relative;
151
+ let start = index;
152
+ if source[index..].starts_with("<!--") {
153
+ index = source[index + 4..]
154
+ .find("-->")
155
+ .map_or(bytes.len(), |end| index + end + 7);
156
+ comments.push_back((start, index));
157
+ continue;
158
+ }
159
+ let mut cursor = index + 1;
160
+ let mut quote = None;
161
+ while cursor < bytes.len() {
162
+ let ch = bytes[cursor];
163
+ if let Some(q) = quote {
164
+ if ch == q {
165
+ quote = None;
166
+ }
167
+ } else if ch == b'\'' || ch == b'"' {
168
+ quote = Some(ch);
169
+ } else if ch == b'>' {
170
+ break;
171
+ }
172
+ cursor += 1;
173
+ }
174
+ if cursor == bytes.len() {
175
+ break;
176
+ }
177
+ index = cursor + 1;
178
+ let token = &source[start + 1..cursor];
179
+ if token.to_ascii_lowercase().starts_with("!doctype") {
180
+ doctypes.push_back((start, index));
181
+ continue;
182
+ }
183
+ if token.starts_with('!') || token.starts_with('?') {
184
+ comments.push_back((start, index));
185
+ continue;
186
+ }
187
+ if !token.starts_with('/')
188
+ && !token
189
+ .as_bytes()
190
+ .first()
191
+ .is_some_and(u8::is_ascii_alphabetic)
192
+ {
193
+ characters.push((source[start..index].to_owned(), start, index, true));
194
+ continue;
195
+ }
196
+ if let Some(close) = token.strip_prefix('/') {
197
+ let name = close
198
+ .split_whitespace()
199
+ .next()
200
+ .unwrap_or("")
201
+ .to_ascii_lowercase();
202
+ if let Some(position) = stack.iter().rposition(|id| elements[*id].name == name) {
203
+ for id in stack.drain(position..) {
204
+ elements[id].end = if elements[id].name == name {
205
+ index
206
+ } else {
207
+ start
208
+ };
209
+ if elements[id].name == name {
210
+ elements[id].end_tag = Some((start, index));
211
+ }
212
+ }
213
+ }
214
+ continue;
215
+ }
216
+ let name_end = token
217
+ .find(|ch: char| ch.is_whitespace() || ch == '/')
218
+ .unwrap_or(token.len());
219
+ if name_end == 0 {
220
+ continue;
221
+ }
222
+ let name = token[..name_end].to_ascii_lowercase();
223
+ if matches!(name.as_str(), "p" | "li" | "option" | "dt" | "dd") {
224
+ if let Some(position) = stack.iter().rposition(|id| elements[*id].name == name) {
225
+ for id in stack.drain(position..) {
226
+ elements[id].end = start;
227
+ }
228
+ }
229
+ }
230
+ let mut attrs = Vec::new();
231
+ let mut at = start + 1 + name_end;
232
+ while at < cursor {
233
+ while at < cursor && bytes[at].is_ascii_whitespace() {
234
+ at += 1;
235
+ }
236
+ if at >= cursor || bytes[at] == b'/' {
237
+ break;
238
+ }
239
+ let attr_start = at;
240
+ while at < cursor
241
+ && !bytes[at].is_ascii_whitespace()
242
+ && !matches!(bytes[at], b'=' | b'/' | b'>')
243
+ {
244
+ at += 1;
245
+ }
246
+ if attr_start == at {
247
+ at += 1;
248
+ continue;
249
+ }
250
+ let attr_name = source[attr_start..at].to_ascii_lowercase();
251
+ let mut attr_end = at;
252
+ while at < cursor && bytes[at].is_ascii_whitespace() {
253
+ at += 1;
254
+ }
255
+ if bytes.get(at) == Some(&b'=') {
256
+ at += 1;
257
+ while at < cursor && bytes[at].is_ascii_whitespace() {
258
+ at += 1;
259
+ }
260
+ if matches!(bytes.get(at), Some(b'\'' | b'"')) {
261
+ let q = bytes[at];
262
+ at += 1;
263
+ while at < cursor && bytes[at] != q {
264
+ at += 1;
265
+ }
266
+ at += (at < cursor) as usize;
267
+ } else {
268
+ while at < cursor && !bytes[at].is_ascii_whitespace() {
269
+ at += 1;
270
+ }
271
+ }
272
+ attr_end = at;
273
+ }
274
+ attrs.push((attr_name, attr_start, attr_end));
275
+ }
276
+ let void = super::void_name(&name) || token.trim_end().ends_with('/');
277
+ let id = elements.len();
278
+ elements.push(Element {
279
+ name,
280
+ start,
281
+ end: if void { index } else { source.len() },
282
+ start_end: index,
283
+ end_tag: None,
284
+ attrs,
285
+ });
286
+ if !void {
287
+ stack.push(id);
288
+ }
289
+ }
290
+ let mut by_name: HashMap<String, VecDeque<Value>> = HashMap::new();
291
+ for element in elements {
292
+ let mut value = range(&positions, element.start, element.end);
293
+ value["startTag"] = range(&positions, element.start, element.start_end);
294
+ if let Some((start, end)) = element.end_tag {
295
+ value["endTag"] = range(&positions, start, end);
296
+ }
297
+ if !element.attrs.is_empty() {
298
+ let mut attrs = Map::new();
299
+ for (name, start, end) in element.attrs {
300
+ attrs
301
+ .entry(name)
302
+ .or_insert_with(|| range(&positions, start, end));
303
+ }
304
+ value["startTag"]["attrs"] = Value::Object(attrs.clone());
305
+ value["attrs"] = Value::Object(attrs);
306
+ }
307
+ by_name.entry(element.name).or_default().push_back(value);
308
+ }
309
+ // Match character tokens to the completed DOM. Foster parenting can merge
310
+ // text across table markup, so consume matching fragments independently of
311
+ // their eventual tree order.
312
+ let mut text: Vec<_> = characters
313
+ .into_iter()
314
+ .map(|(data, start, end, decode)| {
315
+ let data = data
316
+ .replace("\r\n", "\n")
317
+ .replace('\r', "\n")
318
+ .replace('\0', "\u{fffd}");
319
+ let data = if decode {
320
+ crate::arena::decode_entities_mode(&data, false, false, codec)
321
+ } else {
322
+ data
323
+ };
324
+ (data, start, end, false)
325
+ })
326
+ .collect();
327
+ for node in nodes {
328
+ if node.kind == "root" && node.parent.is_some() {
329
+ node.extras.insert("sourceCodeLocation".into(), Value::Null);
330
+ }
331
+ if node.is_element() {
332
+ node.extras.insert("sourceCodeLocation".into(), Value::Null);
333
+ }
334
+ if node.kind == "comment" {
335
+ if let Some((start, end)) = comments.pop_front() {
336
+ node.extras
337
+ .insert("sourceCodeLocation".into(), range(&positions, start, end));
338
+ }
339
+ }
340
+ if node.kind == "directive" {
341
+ if let Some((start, end)) = doctypes.pop_front() {
342
+ node.extras
343
+ .insert("sourceCodeLocation".into(), range(&positions, start, end));
344
+ }
345
+ }
346
+ if node.kind == "text" {
347
+ let mut remaining = node.data.as_deref().unwrap_or("");
348
+ let mut first = None;
349
+ let mut last = 0;
350
+ for (data, start, end, used) in &mut text {
351
+ if *used || data.is_empty() {
352
+ continue;
353
+ }
354
+ let mut part = data.as_str();
355
+ let mut token_start = *start;
356
+ if first.is_none() && part.starts_with('\n') && remaining == &part[1..] {
357
+ part = &part[1..];
358
+ token_start += if source[*start..].starts_with("\r\n") {
359
+ 2
360
+ } else {
361
+ 1
362
+ };
363
+ }
364
+ if remaining.starts_with(part) {
365
+ if first.is_none() {
366
+ first = Some(token_start);
367
+ }
368
+ last = *end;
369
+ remaining = &remaining[part.len()..];
370
+ *used = true;
371
+ if remaining.is_empty() {
372
+ break;
373
+ }
374
+ }
375
+ }
376
+ if remaining.is_empty() {
377
+ if let Some(start) = first {
378
+ node.extras
379
+ .insert("sourceCodeLocation".into(), range(&positions, start, last));
380
+ }
381
+ }
382
+ }
383
+ if let Some(name) = &node.name {
384
+ if let Some(location) = by_name
385
+ .get_mut(&name.to_ascii_lowercase())
386
+ .and_then(VecDeque::pop_front)
387
+ {
388
+ node.extras.insert("sourceCodeLocation".into(), location);
389
+ }
390
+ }
391
+ }
392
+ }
@@ -0,0 +1,189 @@
1
+ //! One process-wide CPU budget, shared by batches and streaming parsers.
2
+ use napi::{Error, Result, Status};
3
+ use napi_derive::napi;
4
+ use rayon::prelude::*;
5
+ use std::sync::atomic::{AtomicU32, Ordering};
6
+ use std::sync::{Arc, Condvar, Mutex, OnceLock};
7
+
8
+ struct Configuration {
9
+ threads: usize,
10
+ started: bool,
11
+ pool: Option<Arc<rayon::ThreadPool>>,
12
+ error: Option<String>,
13
+ }
14
+ struct Runtime {
15
+ configuration: Mutex<Configuration>,
16
+ used: Mutex<u32>,
17
+ wake: Condvar,
18
+ batches: AtomicU32,
19
+ peak: AtomicU32,
20
+ }
21
+ fn runtime() -> &'static Runtime {
22
+ static SHARED: OnceLock<Runtime> = OnceLock::new();
23
+ SHARED.get_or_init(|| {
24
+ let default = std::thread::available_parallelism()
25
+ .map_or(1, usize::from)
26
+ .min(256);
27
+ let requested = std::env::var("RUSTEERIO_THREADS").ok();
28
+ let parsed = requested
29
+ .as_ref()
30
+ .and_then(|value| value.parse::<usize>().ok())
31
+ .filter(|value| (1..=256).contains(value));
32
+ Runtime {
33
+ configuration: Mutex::new(Configuration {
34
+ threads: parsed.unwrap_or(default),
35
+ started: false,
36
+ pool: None,
37
+ error: requested
38
+ .filter(|_| parsed.is_none())
39
+ .map(|_| "RUSTEERIO_THREADS must be an integer from 1 to 256".into()),
40
+ }),
41
+ used: Mutex::new(0),
42
+ wake: Condvar::new(),
43
+ batches: AtomicU32::new(0),
44
+ peak: AtomicU32::new(0),
45
+ }
46
+ })
47
+ }
48
+
49
+ #[napi(object)]
50
+ pub struct WorkerPoolInfo {
51
+ pub threads: u32,
52
+ pub active_batches: u32,
53
+ pub active_workers: u32,
54
+ pub peak_active_workers: u32,
55
+ }
56
+
57
+ #[napi]
58
+ pub fn worker_pool_info() -> WorkerPoolInfo {
59
+ let shared = runtime();
60
+ WorkerPoolInfo {
61
+ threads: shared.configuration.lock().unwrap().threads as u32,
62
+ active_batches: shared.batches.load(Ordering::Relaxed),
63
+ active_workers: *shared.used.lock().unwrap(),
64
+ peak_active_workers: shared.peak.load(Ordering::Relaxed),
65
+ }
66
+ }
67
+
68
+ #[napi]
69
+ pub fn configure_worker_pool(threads: u32) -> Result<WorkerPoolInfo> {
70
+ if !(1..=256).contains(&threads) {
71
+ return Err(Error::new(
72
+ Status::InvalidArg,
73
+ "Worker budget must be an integer from 1 to 256",
74
+ ));
75
+ }
76
+ {
77
+ let mut config = runtime().configuration.lock().unwrap();
78
+ if config.started && config.threads != threads as usize {
79
+ return Err(Error::new(
80
+ Status::InvalidArg,
81
+ "Configure the worker budget before starting native asynchronous work",
82
+ ));
83
+ }
84
+ config.threads = threads as usize;
85
+ config.error = None;
86
+ }
87
+ Ok(worker_pool_info())
88
+ }
89
+
90
+ pub(crate) struct Permit;
91
+ pub(crate) fn permit() -> Result<Permit> {
92
+ let shared = runtime();
93
+ let limit = {
94
+ let mut config = shared.configuration.lock().unwrap();
95
+ if let Some(error) = &config.error {
96
+ return Err(Error::new(Status::InvalidArg, error.clone()));
97
+ }
98
+ config.started = true;
99
+ config.threads as u32
100
+ };
101
+ let mut used = shared.used.lock().unwrap();
102
+ while *used >= limit {
103
+ used = shared.wake.wait(used).unwrap();
104
+ }
105
+ *used += 1;
106
+ shared.peak.fetch_max(*used, Ordering::Relaxed);
107
+ Ok(Permit)
108
+ }
109
+ impl Drop for Permit {
110
+ fn drop(&mut self) {
111
+ let shared = runtime();
112
+ *shared.used.lock().unwrap() -= 1;
113
+ shared.wake.notify_one();
114
+ }
115
+ }
116
+
117
+ pub(crate) struct Batch;
118
+ pub(crate) fn batch() -> Batch {
119
+ runtime().batches.fetch_add(1, Ordering::Relaxed);
120
+ Batch
121
+ }
122
+ impl Drop for Batch {
123
+ fn drop(&mut self) {
124
+ runtime().batches.fetch_sub(1, Ordering::Relaxed);
125
+ }
126
+ }
127
+
128
+ pub(crate) fn parallel_map<T: Sync, U: Send>(
129
+ items: &[T],
130
+ threads: usize,
131
+ operation: impl Fn(&T) -> Result<U> + Sync + Send,
132
+ ) -> Result<Vec<U>> {
133
+ let _batch = batch();
134
+ let pool = {
135
+ let mut config = runtime().configuration.lock().unwrap();
136
+ if let Some(error) = &config.error {
137
+ return Err(Error::new(Status::InvalidArg, error.clone()));
138
+ }
139
+ config.started = true;
140
+ if config.pool.is_none() {
141
+ config.pool = Some(Arc::new(
142
+ rayon::ThreadPoolBuilder::new()
143
+ .num_threads(config.threads)
144
+ .build()
145
+ .map_err(|error| Error::new(Status::GenericFailure, error.to_string()))?,
146
+ ));
147
+ }
148
+ config.pool.as_ref().unwrap().clone()
149
+ };
150
+ let chunk_size = items.len().div_ceil(threads.max(1)).max(1);
151
+ let chunks: Result<Vec<Vec<U>>> = pool.install(|| {
152
+ items
153
+ .par_chunks(chunk_size)
154
+ .map(|chunk| {
155
+ chunk
156
+ .iter()
157
+ .map(|item| {
158
+ let _permit = permit()?;
159
+ operation(item)
160
+ })
161
+ .collect()
162
+ })
163
+ .collect()
164
+ });
165
+ Ok(chunks?.into_iter().flatten().collect())
166
+ }
167
+
168
+ #[cfg(test)]
169
+ mod tests {
170
+ use super::*;
171
+ #[test]
172
+ fn shared_budget_and_order_survive_concurrent_batches() {
173
+ let budget = worker_pool_info().threads;
174
+ let inputs: Vec<_> = (0..32).collect();
175
+ std::thread::scope(|scope| {
176
+ let handles: Vec<_> = (0..4)
177
+ .map(|_| scope.spawn(|| parallel_map(&inputs, 2, |value| Ok(value * 2)).unwrap()))
178
+ .collect();
179
+ for handle in handles {
180
+ assert_eq!(
181
+ handle.join().unwrap(),
182
+ inputs.iter().map(|value| value * 2).collect::<Vec<_>>()
183
+ );
184
+ }
185
+ });
186
+ let info = worker_pool_info();
187
+ assert!(info.peak_active_workers <= budget);
188
+ }
189
+ }