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,1249 @@
1
+ //! WHATWG tokenizer diagnostics coupled to the real HTML5 tree builder.
2
+ //!
3
+ //! The tokenizer reports source-byte spans. The adapter converts them to
4
+ //! parse5's UTF-16 offsets and filters html5ever's additional recovery warnings.
5
+ use std::cell::{Cell, RefCell};
6
+ use std::collections::VecDeque;
7
+ use std::rc::Rc;
8
+
9
+ use html5ever::tokenizer::{
10
+ states::RawKind, Doctype, Tag, TagKind, Token as TreeToken, TokenSink, TokenSinkResult,
11
+ };
12
+ use html5ever::tree_builder::{TreeBuilder, TreeBuilderOpts, TreeSink};
13
+ use html5ever::{Attribute, LocalName, Namespace, QualName};
14
+ use html5gum::emitters::ForwardingEmitter;
15
+ use html5gum::{DefaultEmitter, Emitter, Error, Span, State, Token, Tokenizer};
16
+ use scraper::Html;
17
+ use serde_json::{json, Value};
18
+
19
+ use crate::sink::CompatibleSink;
20
+
21
+ struct PositionedEmitter {
22
+ inner: DefaultEmitter<usize>,
23
+ foreign: Rc<Cell<bool>>,
24
+ context: Rc<DiagnosticContext>,
25
+ position: usize,
26
+ text_kind: Option<u8>,
27
+ text_end: usize,
28
+ boundaries: VecDeque<usize>,
29
+ deferred_text: Option<Token<usize>>,
30
+ queued: Option<Token<usize>>,
31
+ raw_queue: VecDeque<Token<usize>>,
32
+ eof: bool,
33
+ }
34
+ fn character_kind(byte: u8) -> u8 {
35
+ match byte {
36
+ 0 => 0,
37
+ b' ' | b'\t' | b'\n' | b'\r' | 12 => 1,
38
+ _ => 2,
39
+ }
40
+ }
41
+ impl PositionedEmitter {
42
+ fn dispatch(&mut self, token: Token<usize>) -> Token<usize> {
43
+ match &token {
44
+ Token::String(text) => self.context.text_non_whitespace.set(
45
+ text.value
46
+ .iter()
47
+ .any(|byte| character_kind(*byte) == 2)
48
+ .then_some(text.span.start),
49
+ ),
50
+ Token::Error(_) => {}
51
+ _ => {
52
+ self.text_kind = None;
53
+ self.text_end = self.position;
54
+ }
55
+ }
56
+ token
57
+ }
58
+ }
59
+ impl ForwardingEmitter for PositionedEmitter {
60
+ type Token = Token<usize>;
61
+ fn inner(&mut self) -> &mut impl Emitter<Token = Self::Token> {
62
+ &mut self.inner
63
+ }
64
+ fn adjusted_current_node_present_but_not_in_html_namespace(&mut self) -> bool {
65
+ self.foreign.get()
66
+ }
67
+ fn rusteerio_codec(&mut self) -> bool {
68
+ self.context.codec_enabled()
69
+ }
70
+ fn move_position(&mut self, offset: isize) {
71
+ self.position = self.position.checked_add_signed(offset).unwrap();
72
+ Emitter::move_position(&mut self.inner, offset);
73
+ }
74
+ fn init_string(&mut self) {
75
+ if self.text_kind.is_none() {
76
+ self.text_end = self.position;
77
+ }
78
+ Emitter::init_string(&mut self.inner);
79
+ }
80
+ fn emit_string(&mut self, characters: &[u8]) {
81
+ let raw_start = self.text_end;
82
+ let raw = self.context.source_between(raw_start, self.position);
83
+ let direct = raw.as_bytes() == characters;
84
+ let mut index = 0;
85
+ while index < characters.len() {
86
+ let kind = character_kind(characters[index]);
87
+ let mut end = index + 1;
88
+ while end < characters.len() && character_kind(characters[end]) == kind {
89
+ end += 1;
90
+ }
91
+ let start_position = if direct { raw_start + index } else { raw_start };
92
+ let end_position = if direct {
93
+ raw_start + end
94
+ } else {
95
+ self.position
96
+ };
97
+ if self.text_kind.is_some_and(|previous| previous != kind) {
98
+ self.boundaries.push_back(start_position);
99
+ Emitter::init_comment(&mut self.inner); // Flush the completed character run.
100
+ let delta = start_position as isize - self.position as isize;
101
+ Emitter::move_position(&mut self.inner, delta);
102
+ Emitter::init_string(&mut self.inner);
103
+ Emitter::move_position(&mut self.inner, -delta);
104
+ }
105
+ self.text_kind = Some(kind);
106
+ let delta = end_position as isize - self.position as isize;
107
+ Emitter::move_position(&mut self.inner, delta);
108
+ Emitter::emit_string(&mut self.inner, &characters[index..end]);
109
+ Emitter::move_position(&mut self.inner, -delta);
110
+ index = end;
111
+ }
112
+ self.text_end = self.position;
113
+ }
114
+ fn emit_char_ref(&mut self, characters: &[u8], is_attribute: bool) {
115
+ let encoded;
116
+ let characters = if self.context.codec_enabled() && characters.contains(&0xee) {
117
+ encoded = crate::codec::encode_literal(&text(characters));
118
+ encoded.as_bytes()
119
+ } else {
120
+ characters
121
+ };
122
+ if is_attribute {
123
+ Emitter::push_attribute_value(&mut self.inner, characters);
124
+ } else {
125
+ Emitter::emit_string(self, characters);
126
+ }
127
+ }
128
+ fn pop_token(&mut self) -> Option<Self::Token> {
129
+ if let Some(token) = self.queued.take() {
130
+ return Some(self.dispatch(token));
131
+ }
132
+ while let Some(token) = self
133
+ .raw_queue
134
+ .pop_front()
135
+ .or_else(|| Emitter::pop_token(&mut self.inner))
136
+ {
137
+ if let Token::Error(error) = &token {
138
+ if matches!(
139
+ error.value,
140
+ Error::ControlCharacterInInputStream
141
+ | Error::NoncharacterInInputStream
142
+ | Error::SurrogateInInputStream
143
+ ) {
144
+ let point = error.span.start.saturating_sub(1);
145
+ let mut prior = VecDeque::new();
146
+ let mut remaining = VecDeque::new();
147
+ while let Some(next) = self
148
+ .raw_queue
149
+ .pop_front()
150
+ .or_else(|| Emitter::pop_token(&mut self.inner))
151
+ {
152
+ if matches!(&next,Token::String(text) if self.boundaries.contains(&text.span.end) && text.span.end<=point)
153
+ {
154
+ prior.push_back(next);
155
+ } else {
156
+ remaining.push_back(next);
157
+ }
158
+ }
159
+ if !prior.is_empty() {
160
+ prior.push_back(token);
161
+ prior.append(&mut remaining);
162
+ self.raw_queue = prior;
163
+ continue;
164
+ }
165
+ self.raw_queue = remaining;
166
+ }
167
+ }
168
+ match &token {
169
+ Token::String(text) if self.boundaries.front() == Some(&text.span.end) => {
170
+ self.boundaries.pop_front();
171
+ return Some(self.dispatch(token));
172
+ }
173
+ Token::String(_) => self.deferred_text = Some(token),
174
+ Token::Error(_) => return Some(token),
175
+ _ => {
176
+ if let Some(text) = self.deferred_text.take() {
177
+ self.queued = Some(token);
178
+ return Some(self.dispatch(text));
179
+ }
180
+ return Some(self.dispatch(token));
181
+ }
182
+ }
183
+ }
184
+ if self.eof {
185
+ self.deferred_text.take().map(|token| self.dispatch(token))
186
+ } else {
187
+ None
188
+ }
189
+ }
190
+ fn emit_eof(&mut self) {
191
+ Emitter::emit_eof(&mut self.inner);
192
+ self.eof = true;
193
+ }
194
+ fn emit_error(&mut self, error: Error) {
195
+ // html5gum validates fast-path byte chunks before advancing its emitter
196
+ // position. Recover each input-code-point position from the same bytes.
197
+ if let Some(point) = self.context.input_error_offset(error) {
198
+ let delta = (point + 1) as isize - self.position as isize;
199
+ Emitter::move_position(&mut self.inner, delta);
200
+ Emitter::emit_error(&mut self.inner, error);
201
+ Emitter::move_position(&mut self.inner, -delta);
202
+ } else {
203
+ Emitter::emit_error(&mut self.inner, error);
204
+ }
205
+ }
206
+ }
207
+
208
+ #[derive(Clone, Copy, Default)]
209
+ struct Point {
210
+ line: usize,
211
+ column: usize,
212
+ offset: usize,
213
+ }
214
+ struct Checkpoint {
215
+ byte: usize,
216
+ point: Point,
217
+ constant: bool,
218
+ }
219
+ struct Positions {
220
+ checkpoints: Vec<Checkpoint>,
221
+ length: usize,
222
+ last: Point,
223
+ previous_cr: bool,
224
+ codec: bool,
225
+ escaped: bool,
226
+ }
227
+ impl Positions {
228
+ fn new(source: &str) -> Self {
229
+ let point = Point {
230
+ line: 1,
231
+ column: 1,
232
+ offset: 0,
233
+ };
234
+ let mut positions = Self {
235
+ checkpoints: vec![Checkpoint {
236
+ byte: 0,
237
+ point,
238
+ constant: false,
239
+ }],
240
+ length: 0,
241
+ last: point,
242
+ previous_cr: false,
243
+ codec: false,
244
+ escaped: false,
245
+ };
246
+ positions.append(source);
247
+ positions
248
+ }
249
+ fn checkpoint(&mut self, byte: usize, point: Point, constant: bool) {
250
+ if self
251
+ .checkpoints
252
+ .last()
253
+ .is_some_and(|entry| entry.byte == byte)
254
+ {
255
+ let entry = self.checkpoints.last_mut().unwrap();
256
+ entry.point = point;
257
+ entry.constant = constant;
258
+ } else {
259
+ self.checkpoints.push(Checkpoint {
260
+ byte,
261
+ point,
262
+ constant,
263
+ });
264
+ }
265
+ }
266
+ fn append(&mut self, source: &str) {
267
+ let base = self.length;
268
+ let mut point = self.last;
269
+ for (index, character) in source.char_indices() {
270
+ if !character.is_ascii() {
271
+ self.checkpoint(base + index, point, true);
272
+ }
273
+ let width = if self.codec && !self.escaped && character == crate::codec::ESCAPE {
274
+ self.escaped = true;
275
+ 0
276
+ } else {
277
+ self.escaped = false;
278
+ character.len_utf16()
279
+ };
280
+ point.offset += width;
281
+ if character == '\r' {
282
+ point.line += 1;
283
+ point.column = 1;
284
+ } else if character == '\n' {
285
+ if !self.previous_cr {
286
+ point.line += 1;
287
+ }
288
+ point.column = 1;
289
+ } else {
290
+ point.column += width;
291
+ }
292
+ self.previous_cr = character == '\r';
293
+ if !character.is_ascii() || matches!(character, '\r' | '\n') {
294
+ self.checkpoint(base + index + character.len_utf8(), point, false);
295
+ }
296
+ }
297
+ self.length += source.len();
298
+ self.last = point;
299
+ }
300
+ fn point(&self, offset: usize) -> Point {
301
+ let offset = offset.min(self.length);
302
+ let index = self
303
+ .checkpoints
304
+ .partition_point(|entry| entry.byte <= offset)
305
+ .saturating_sub(1);
306
+ let entry = &self.checkpoints[index];
307
+ let mut point = entry.point;
308
+ if !entry.constant {
309
+ let delta = offset - entry.byte;
310
+ point.offset += delta;
311
+ point.column += delta;
312
+ }
313
+ point
314
+ }
315
+ fn record(&self, code: &str, start: usize, end: usize) -> Value {
316
+ let start = self.point(start);
317
+ let end = self.point(end);
318
+ json!({"code":code,"startLine":start.line,"startCol":start.column,"startOffset":start.offset,
319
+ "endLine":end.line,"endCol":end.column,"endOffset":end.offset})
320
+ }
321
+ }
322
+
323
+ #[derive(Clone, Copy)]
324
+ enum Kind<'a> {
325
+ Start(&'a str),
326
+ End(&'a str),
327
+ Text,
328
+ Comment,
329
+ Doctype,
330
+ Eof,
331
+ }
332
+
333
+ fn tree_code(message: &str, kind: Kind<'_>) -> Option<&'static str> {
334
+ if message.contains("insertion mode Initial") {
335
+ return Some("missing-doctype");
336
+ }
337
+ if message.starts_with("Bad DOCTYPE") {
338
+ return Some("non-conforming-doctype");
339
+ }
340
+ if message.starts_with("DOCTYPE in")
341
+ && ["BeforeHead", "InHead", "AfterHead"]
342
+ .iter()
343
+ .any(|mode| message.contains(mode))
344
+ {
345
+ return Some("misplaced-doctype");
346
+ }
347
+ if message == "Unacknowledged self-closing tag" {
348
+ return Some("non-void-html-element-start-tag-with-trailing-solidus");
349
+ }
350
+ if message.contains("Unexpected open element while closing") && message.contains("template") {
351
+ return Some("closing-of-element-with-open-child-elements");
352
+ }
353
+ if message.contains("insertion mode Text") && matches!(kind, Kind::Eof) {
354
+ return Some("eof-in-element-that-can-contain-only-text");
355
+ }
356
+ if message.contains("insertion mode InHeadNoscript") {
357
+ return Some(match kind {
358
+ Kind::Start("noscript") => "nested-noscript-in-head",
359
+ Kind::Start("head") => "misplaced-start-tag-for-head-element",
360
+ Kind::End(name) if name != "br" => "end-tag-without-matching-open-element",
361
+ Kind::Eof => "open-elements-left-after-eof",
362
+ _ => "disallowed-content-in-noscript-in-head",
363
+ });
364
+ }
365
+ if matches!(kind, Kind::End("template")) {
366
+ return None;
367
+ }
368
+ if message.contains("insertion mode BeforeHead") && matches!(kind, Kind::End(_)) {
369
+ return Some("end-tag-without-matching-open-element");
370
+ }
371
+ if message.contains("insertion mode InHead") || message.contains("insertion mode AfterHead") {
372
+ return match kind {
373
+ Kind::Start("head") => Some("misplaced-start-tag-for-head-element"),
374
+ Kind::Start(_) if message.contains("insertion mode AfterHead") => {
375
+ Some("abandoned-head-element-child")
376
+ }
377
+ Kind::End(_) => Some("end-tag-without-matching-open-element"),
378
+ _ => None,
379
+ };
380
+ }
381
+ None
382
+ }
383
+
384
+ fn token_error_offset(source: &str, error: Error, span: Span<usize>) -> usize {
385
+ let code = error.as_str();
386
+ if code.starts_with("eof-") {
387
+ return span.start.min(source.len());
388
+ }
389
+ if matches!(
390
+ error,
391
+ Error::MissingSemicolonAfterCharacterReference
392
+ | Error::AbsenceOfDigitsInNumericCharacterReference
393
+ | Error::NullCharacterReference
394
+ | Error::SurrogateCharacterReference
395
+ | Error::CharacterReferenceOutsideUnicodeRange
396
+ | Error::NoncharacterCharacterReference
397
+ | Error::ControlCharacterReference
398
+ ) {
399
+ let mut boundary = span.start.min(source.len());
400
+ while !source.is_char_boundary(boundary) {
401
+ boundary -= 1;
402
+ }
403
+ if let Some(amp) = source[..boundary].rfind('&') {
404
+ if let Some((end, _, _)) = character_reference(source, amp) {
405
+ return end;
406
+ }
407
+ }
408
+ }
409
+ if error == Error::DuplicateAttribute {
410
+ return span.end.min(source.len());
411
+ }
412
+ // State-machine errors follow a consumed byte; parse5 points at the current
413
+ // code point. Entity errors above instead point at the consumed reference.
414
+ span.start.saturating_sub(1)
415
+ }
416
+
417
+ fn character_reference(source: &str, amp: usize) -> Option<(usize, u32, u32)> {
418
+ let bytes = source.as_bytes();
419
+ let mut index = amp + 1;
420
+ if bytes.get(index) == Some(&b'#') {
421
+ index += 1;
422
+ let hexadecimal = matches!(bytes.get(index), Some(b'x' | b'X'));
423
+ if hexadecimal {
424
+ index += 1;
425
+ }
426
+ let start = index;
427
+ while bytes.get(index).is_some_and(|byte| {
428
+ if hexadecimal {
429
+ byte.is_ascii_hexdigit()
430
+ } else {
431
+ byte.is_ascii_digit()
432
+ }
433
+ }) {
434
+ index += 1;
435
+ }
436
+ if index == start {
437
+ return Some((index, 0, 0));
438
+ }
439
+ let number = u32::from_str_radix(&source[start..index], if hexadecimal { 16 } else { 10 })
440
+ .unwrap_or(0);
441
+ if bytes.get(index) == Some(&b';') {
442
+ index += 1;
443
+ }
444
+ return Some((index, number, 0));
445
+ }
446
+ let start = index;
447
+ while bytes.get(index).is_some_and(u8::is_ascii_alphanumeric) {
448
+ index += 1;
449
+ }
450
+ if bytes.get(index) == Some(&b';') {
451
+ index += 1;
452
+ }
453
+ for end in (start + 1..=index).rev() {
454
+ if let Some(&(first, second)) = html5ever::data::NAMED_ENTITIES.get(&source[start..end]) {
455
+ if first != 0 {
456
+ return Some((end, first, second));
457
+ }
458
+ }
459
+ }
460
+ None
461
+ }
462
+
463
+ fn first_non_whitespace(source: &str, mut index: usize, end: usize) -> usize {
464
+ while index < end {
465
+ if matches!(source.as_bytes()[index], b' ' | b'\t' | b'\n' | b'\r' | 12) {
466
+ index += 1;
467
+ continue;
468
+ }
469
+ if source.as_bytes()[index] == b'&' {
470
+ if let Some((after, first, second)) = character_reference(source, index) {
471
+ let whitespace = |value| matches!(value, 9 | 10 | 12 | 13 | 32);
472
+ if after <= end && whitespace(first) && (second == 0 || whitespace(second)) {
473
+ index = after;
474
+ continue;
475
+ }
476
+ }
477
+ }
478
+ break;
479
+ }
480
+ index
481
+ }
482
+
483
+ #[derive(Clone, Debug)]
484
+ pub(crate) struct DiagnosticToken {
485
+ pub kind: &'static str,
486
+ pub name: Option<String>,
487
+ pub span: (usize, usize),
488
+ pub attributes: Vec<(String, usize, usize)>,
489
+ }
490
+
491
+ pub(crate) struct DiagnosticContext {
492
+ source: RefCell<String>,
493
+ source_len: Cell<usize>,
494
+ source_base: Cell<usize>,
495
+ keep_source: bool,
496
+ track_positions: bool,
497
+ positions: RefCell<Positions>,
498
+ current: RefCell<DiagnosticToken>,
499
+ token_error: RefCell<Option<Value>>,
500
+ text_non_whitespace: Cell<Option<usize>>,
501
+ input_error_cursor: Cell<usize>,
502
+ is_document: Cell<bool>,
503
+ codec: Cell<bool>,
504
+ }
505
+ impl DiagnosticContext {
506
+ pub(crate) fn new(source: &str) -> Self {
507
+ Self {
508
+ source: RefCell::new(source.to_owned()),
509
+ source_len: Cell::new(source.len()),
510
+ source_base: Cell::new(0),
511
+ keep_source: true,
512
+ track_positions: true,
513
+ positions: RefCell::new(Positions::new(source)),
514
+ current: RefCell::new(DiagnosticToken {
515
+ kind: "eof",
516
+ name: None,
517
+ span: (0, 0),
518
+ attributes: Vec::new(),
519
+ }),
520
+ token_error: RefCell::new(None),
521
+ text_non_whitespace: Cell::new(None),
522
+ input_error_cursor: Cell::new(0),
523
+ is_document: Cell::new(true),
524
+ codec: Cell::new(false),
525
+ }
526
+ }
527
+ #[cfg(test)]
528
+ pub(crate) fn streaming(keep_source: bool) -> Self {
529
+ Self::streaming_with_source_mode(keep_source, keep_source)
530
+ }
531
+ pub(crate) fn streaming_with_source_mode(keep_source: bool, track_positions: bool) -> Self {
532
+ let mut context = Self::new("");
533
+ context.keep_source = keep_source;
534
+ context.track_positions = track_positions;
535
+ context
536
+ }
537
+ pub(crate) fn append_source(&self, chunk: &str) {
538
+ self.source_len.set(self.source_len.get() + chunk.len());
539
+ if self.track_positions {
540
+ self.positions.borrow_mut().append(chunk);
541
+ }
542
+ if self.keep_source || self.track_positions {
543
+ const WINDOW: usize = 64 * 1024;
544
+ let mut source = self.source.borrow_mut();
545
+ if !self.keep_source && chunk.len() >= WINDOW {
546
+ let mut start = chunk.len() - WINDOW;
547
+ while !chunk.is_char_boundary(start) {
548
+ start += 1;
549
+ }
550
+ *source = chunk[start..].to_owned();
551
+ } else {
552
+ source.push_str(chunk);
553
+ if !self.keep_source && source.len() > WINDOW {
554
+ let mut start = source.len() - WINDOW;
555
+ while !source.is_char_boundary(start) {
556
+ start += 1;
557
+ }
558
+ source.drain(..start);
559
+ }
560
+ }
561
+ self.source_base.set(self.source_len.get() - source.len());
562
+ }
563
+ }
564
+ pub(crate) fn source_len(&self) -> usize {
565
+ self.source_len.get()
566
+ }
567
+ pub(crate) fn codec_enabled(&self) -> bool {
568
+ self.codec.get()
569
+ }
570
+ fn input_error_offset(&self, error: Error) -> Option<usize> {
571
+ if !matches!(
572
+ error,
573
+ Error::ControlCharacterInInputStream
574
+ | Error::NoncharacterInInputStream
575
+ | Error::SurrogateInInputStream
576
+ ) {
577
+ return None;
578
+ }
579
+ let source = self.source.borrow();
580
+ let base = self.source_base.get();
581
+ let mut start = self
582
+ .input_error_cursor
583
+ .get()
584
+ .saturating_sub(base)
585
+ .min(source.len());
586
+ while !source.is_char_boundary(start) {
587
+ start += 1;
588
+ }
589
+ let mut escaped = false;
590
+ for (index, character) in source[start..].char_indices() {
591
+ let value = character as u32;
592
+ if self.codec_enabled() && character == crate::codec::ESCAPE && !escaped {
593
+ escaped = true;
594
+ continue;
595
+ }
596
+ let encoded_surrogate =
597
+ self.codec_enabled() && !escaped && (0xe000..0xe800).contains(&value);
598
+ escaped = false;
599
+ let matches = match error {
600
+ Error::ControlCharacterInInputStream => {
601
+ matches!(value,1..=8 | 11 | 14..=31 | 127..=159)
602
+ }
603
+ Error::NoncharacterInInputStream => {
604
+ matches!(value, 0xfdd0..=0xfdef) || value & 0xffff >= 0xfffe
605
+ }
606
+ _ => encoded_surrogate,
607
+ };
608
+ if matches {
609
+ let point = base + start + index;
610
+ self.input_error_cursor.set(point + character.len_utf8());
611
+ return Some(point);
612
+ }
613
+ }
614
+ None
615
+ }
616
+ pub(crate) fn source_between(&self, start: usize, end: usize) -> String {
617
+ let base = self.source_base.get();
618
+ if start < base || end < base {
619
+ return String::new();
620
+ }
621
+ self.source
622
+ .borrow()
623
+ .get(start - base..end - base)
624
+ .unwrap_or("")
625
+ .to_owned()
626
+ }
627
+ pub(crate) fn location(&self, start: usize, end: usize) -> Value {
628
+ let mut value = self.positions.borrow().record("", start, end);
629
+ value.as_object_mut().unwrap().remove("code");
630
+ value
631
+ }
632
+ pub(crate) fn current_span(&self) -> (usize, usize) {
633
+ self.current.borrow().span
634
+ }
635
+ pub(crate) fn current_token(&self) -> DiagnosticToken {
636
+ self.current.borrow().clone()
637
+ }
638
+ fn set(
639
+ &self,
640
+ kind: &'static str,
641
+ name: Option<String>,
642
+ span: Span<usize>,
643
+ attributes: Vec<(String, usize, usize)>,
644
+ ) {
645
+ *self.current.borrow_mut() = DiagnosticToken {
646
+ kind,
647
+ name,
648
+ span: (span.start, span.end),
649
+ attributes,
650
+ };
651
+ self.token_error.borrow_mut().take();
652
+ }
653
+ pub(crate) fn error(&self, message: &str) -> Option<Value> {
654
+ if let Some(error) = self.token_error.borrow().as_ref() {
655
+ if error["code"] == message {
656
+ return Some(error.clone());
657
+ }
658
+ }
659
+ let token = self.current.borrow();
660
+ let kind = match token.kind {
661
+ "startTag" => Kind::Start(token.name.as_deref().unwrap_or("")),
662
+ "endTag" => Kind::End(token.name.as_deref().unwrap_or("")),
663
+ "doctype" => Kind::Doctype,
664
+ "comment" => Kind::Comment,
665
+ "eof" => Kind::Eof,
666
+ _ => Kind::Text,
667
+ };
668
+ let code = if !self.is_document.get()
669
+ && matches!(kind, Kind::End("template"))
670
+ && message.starts_with("Unexpected token")
671
+ {
672
+ "end-tag-without-matching-open-element"
673
+ } else {
674
+ tree_code(message, kind)?
675
+ };
676
+ let source = self.source.borrow();
677
+ let base = self.source_base.get();
678
+ let mut start = token.span.0;
679
+ if code == "missing-doctype" && matches!(kind, Kind::Text) {
680
+ let end = token.span.1.saturating_sub(base).min(source.len());
681
+ if start >= base {
682
+ start = base + first_non_whitespace(&source, start - base, end);
683
+ } else if let Some(first) = self.text_non_whitespace.get() {
684
+ start = first;
685
+ }
686
+ }
687
+ let end = if code == "missing-doctype" {
688
+ start
689
+ } else {
690
+ token.span.1
691
+ };
692
+ Some(self.positions.borrow().record(code, start, end))
693
+ }
694
+ }
695
+
696
+ fn text(value: &[u8]) -> String {
697
+ String::from_utf8_lossy(value).into_owned()
698
+ }
699
+ fn apply_state<R: html5gum::Reader, H>(
700
+ tokenizer: &mut Tokenizer<R, PositionedEmitter>,
701
+ state: TokenSinkResult<H>,
702
+ ) {
703
+ match state {
704
+ TokenSinkResult::Plaintext => tokenizer.set_state(State::PlainText),
705
+ TokenSinkResult::RawData(RawKind::Rcdata) => tokenizer.set_state(State::RcData),
706
+ TokenSinkResult::RawData(RawKind::Rawtext) => tokenizer.set_state(State::RawText),
707
+ TokenSinkResult::RawData(RawKind::ScriptData | RawKind::ScriptDataEscaped(_)) => {
708
+ tokenizer.set_state(State::ScriptData)
709
+ }
710
+ TokenSinkResult::Script(_) => tokenizer.set_state(State::Data),
711
+ TokenSinkResult::Continue => {}
712
+ }
713
+ }
714
+
715
+ /// Drive the actual user sink. Its parse_error callback can call context.error()
716
+ /// synchronously; callback mutations therefore affect this same tree builder.
717
+ pub(crate) fn parse_with_sink<S, F>(
718
+ source: &str,
719
+ is_document: bool,
720
+ options: &Value,
721
+ sink: S,
722
+ context: Rc<DiagnosticContext>,
723
+ on_step: F,
724
+ ) -> S::Output
725
+ where
726
+ S: TreeSink,
727
+ F: FnMut(&S, &DiagnosticContext),
728
+ {
729
+ use html5gum::Readable;
730
+ match parse_reader_with_sink(
731
+ source.to_reader(),
732
+ is_document,
733
+ options,
734
+ sink,
735
+ context,
736
+ on_step,
737
+ ) {
738
+ Ok(output) => output,
739
+ Err(error) => match error {},
740
+ }
741
+ }
742
+
743
+ /// Reader variant supports a blocking streaming reader owned by a parsing
744
+ /// thread. html5gum does not support suspending/retrying a partial reader state.
745
+ pub(crate) fn parse_reader_with_sink<R, S, F>(
746
+ reader: R,
747
+ is_document: bool,
748
+ options: &Value,
749
+ sink: S,
750
+ context: Rc<DiagnosticContext>,
751
+ mut on_step: F,
752
+ ) -> Result<S::Output, R::Error>
753
+ where
754
+ R: html5gum::Reader,
755
+ S: TreeSink,
756
+ F: FnMut(&S, &DiagnosticContext),
757
+ {
758
+ context.is_document.set(is_document);
759
+ if options.get("__utf16Codec") == Some(&Value::Bool(true)) {
760
+ context.codec.set(true);
761
+ let mut positions = Positions::new("");
762
+ positions.codec = true;
763
+ positions.append(&context.source.borrow());
764
+ *context.positions.borrow_mut() = positions;
765
+ }
766
+ let scripting_enabled = options.get("scriptingEnabled") != Some(&Value::Bool(false));
767
+ let builder_options = TreeBuilderOpts {
768
+ exact_errors: true,
769
+ scripting_enabled,
770
+ ..Default::default()
771
+ };
772
+ let context_name = options
773
+ .get("__contextName")
774
+ .and_then(Value::as_str)
775
+ .unwrap_or("template");
776
+ let tree = if is_document {
777
+ TreeBuilder::new(sink, builder_options)
778
+ } else {
779
+ let element = html5ever::interface::create_element(
780
+ &sink,
781
+ QualName::new(
782
+ None,
783
+ Namespace::from(
784
+ options
785
+ .get("__contextNamespace")
786
+ .and_then(Value::as_str)
787
+ .unwrap_or(crate::HTML_NS),
788
+ ),
789
+ LocalName::from(context_name),
790
+ ),
791
+ Vec::new(),
792
+ );
793
+ TreeBuilder::new_for_fragment(sink, element, None, builder_options)
794
+ };
795
+ let foreign = Rc::new(Cell::new(false));
796
+ let mut inner = DefaultEmitter::new_with_span();
797
+ if !is_document {
798
+ Emitter::set_last_start_tag(&mut inner, Some(context_name.as_bytes()));
799
+ }
800
+ let emitter = PositionedEmitter {
801
+ inner,
802
+ foreign: foreign.clone(),
803
+ context: context.clone(),
804
+ position: 0,
805
+ text_kind: None,
806
+ text_end: 0,
807
+ boundaries: VecDeque::new(),
808
+ deferred_text: None,
809
+ queued: None,
810
+ raw_queue: VecDeque::new(),
811
+ eof: false,
812
+ };
813
+ let mut tokenizer = Tokenizer::new_with_emitter(reader, emitter);
814
+ if !is_document {
815
+ use html5ever::tokenizer::states::State as HtmlState;
816
+ match tree.tokenizer_state_for_context_elem(scripting_enabled) {
817
+ HtmlState::Plaintext => tokenizer.set_state(State::PlainText),
818
+ HtmlState::RawData(kind) => {
819
+ apply_state(&mut tokenizer, TokenSinkResult::<S::Handle>::RawData(kind))
820
+ }
821
+ _ => {}
822
+ }
823
+ }
824
+ while let Some(token) = tokenizer.next() {
825
+ let (inputs, span, kind, name, attributes) = match token? {
826
+ Token::Error(error) => {
827
+ let base = context.source_base.get();
828
+ let point = if context.track_positions && error.span.start >= base {
829
+ base + token_error_offset(
830
+ &context.source.borrow(),
831
+ error.value,
832
+ Span {
833
+ start: error.span.start - base,
834
+ end: error.span.end.saturating_sub(base),
835
+ },
836
+ )
837
+ } else if error.value.as_str().starts_with("eof-") {
838
+ error.span.start
839
+ } else if error.value == Error::DuplicateAttribute {
840
+ error.span.end
841
+ } else {
842
+ error.span.start.saturating_sub(1)
843
+ };
844
+ context.set("error", None, error.span, Vec::new());
845
+ *context.token_error.borrow_mut() = Some(context.positions.borrow().record(
846
+ error.value.as_str(),
847
+ point,
848
+ point,
849
+ ));
850
+ tree.sink.parse_error(error.value.as_str().into());
851
+ on_step(&tree.sink, &context);
852
+ continue;
853
+ }
854
+ Token::StartTag(tag) => {
855
+ let name = text(&tag.name);
856
+ let mut entries: Vec<_> = tag.attributes.into_iter().collect();
857
+ entries.sort_by_key(|(_, value)| value.span.start);
858
+ let attributes = entries
859
+ .iter()
860
+ .map(|(name, value)| (text(name), value.span.start, value.span.end))
861
+ .collect();
862
+ let attrs = entries
863
+ .into_iter()
864
+ .map(|(name, value)| Attribute {
865
+ name: QualName::new(
866
+ None,
867
+ Namespace::from(""),
868
+ LocalName::from(text(&name)),
869
+ ),
870
+ value: text(&value.value).into(),
871
+ })
872
+ .collect();
873
+ (
874
+ vec![TreeToken::TagToken(Tag {
875
+ kind: TagKind::StartTag,
876
+ name: LocalName::from(name.clone()),
877
+ self_closing: tag.self_closing,
878
+ attrs,
879
+ })],
880
+ tag.span,
881
+ "startTag",
882
+ Some(name),
883
+ attributes,
884
+ )
885
+ }
886
+ Token::EndTag(tag) => {
887
+ let name = text(&tag.name);
888
+ (
889
+ vec![TreeToken::TagToken(Tag {
890
+ kind: TagKind::EndTag,
891
+ name: LocalName::from(name.clone()),
892
+ self_closing: false,
893
+ attrs: Vec::new(),
894
+ })],
895
+ tag.span,
896
+ "endTag",
897
+ Some(name),
898
+ Vec::new(),
899
+ )
900
+ }
901
+ Token::Doctype(doctype) => (
902
+ vec![TreeToken::DoctypeToken(Doctype {
903
+ name: Some(text(&doctype.name).into()),
904
+ public_id: doctype
905
+ .public_identifier
906
+ .as_ref()
907
+ .map(|value| text(value).into()),
908
+ system_id: doctype
909
+ .system_identifier
910
+ .as_ref()
911
+ .map(|value| text(value).into()),
912
+ force_quirks: doctype.force_quirks,
913
+ })],
914
+ doctype.span,
915
+ "doctype",
916
+ None,
917
+ Vec::new(),
918
+ ),
919
+ Token::Comment(comment) => (
920
+ vec![TreeToken::CommentToken(text(&comment.value).into())],
921
+ comment.span,
922
+ "comment",
923
+ None,
924
+ Vec::new(),
925
+ ),
926
+ Token::String(value) => {
927
+ let value_text = text(&value.value);
928
+ let mut inputs = Vec::new();
929
+ for (index, part) in value_text.split('\0').enumerate() {
930
+ if index > 0 {
931
+ inputs.push(TreeToken::NullCharacterToken);
932
+ }
933
+ if !part.is_empty() {
934
+ inputs.push(TreeToken::CharacterTokens(part.to_owned().into()));
935
+ }
936
+ }
937
+ (inputs, value.span, "text", None, Vec::new())
938
+ }
939
+ };
940
+ context.set(kind, name, span, attributes);
941
+ for input in inputs {
942
+ let line = context.positions.borrow().point(span.start).line as u64;
943
+ let state = tree.process_token(input, line);
944
+ on_step(&tree.sink, &context);
945
+ foreign.set(tree.adjusted_current_node_present_but_not_in_html_namespace());
946
+ apply_state(&mut tokenizer, state);
947
+ }
948
+ }
949
+ let end = context.source_len();
950
+ context.set("eof", None, Span { start: end, end }, Vec::new());
951
+ let _ = tree.process_token(
952
+ TreeToken::EOFToken,
953
+ context.positions.borrow().point(end).line as u64,
954
+ );
955
+ on_step(&tree.sink, &context);
956
+ tree.end();
957
+ Ok(tree.sink.finish())
958
+ }
959
+
960
+ pub(crate) fn collect(source: &str, is_document: bool, options: &Value) -> Vec<Value> {
961
+ let context = Rc::new(DiagnosticContext::new(source));
962
+ let sink = CompatibleSink::with_errors(if is_document {
963
+ Html::new_document()
964
+ } else {
965
+ Html::new_fragment()
966
+ });
967
+ let mut errors = Vec::new();
968
+ parse_with_sink(
969
+ source,
970
+ is_document,
971
+ options,
972
+ sink,
973
+ context,
974
+ |sink, context| {
975
+ for message in sink.take_errors() {
976
+ if let Some(error) = context.error(&message) {
977
+ errors.push(error);
978
+ }
979
+ }
980
+ },
981
+ );
982
+ errors
983
+ }
984
+
985
+ #[cfg(test)]
986
+ mod tests {
987
+ use super::*;
988
+
989
+ #[test]
990
+ fn utf16_offsets_and_crlf_match_parse5() {
991
+ let errors = collect("<!doctype html>😀\r\n<div a=>", true, &json!({}));
992
+ assert_eq!(
993
+ errors,
994
+ vec![
995
+ json!({"code":"missing-attribute-value","startLine":2,"endLine":2,"startCol":8,"endCol":8,"startOffset":26,"endOffset":26})
996
+ ]
997
+ );
998
+ }
999
+
1000
+ #[test]
1001
+ fn token_errors_precede_missing_doctype_and_keep_real_positions() {
1002
+ let errors = collect("\n<p a=1 a=2>", true, &json!({}));
1003
+ assert_eq!(
1004
+ errors,
1005
+ vec![
1006
+ json!({"code":"duplicate-attribute","startLine":2,"endLine":2,"startCol":9,"endCol":9,"startOffset":9,"endOffset":9}),
1007
+ json!({"code":"missing-doctype","startLine":2,"endLine":2,"startCol":1,"endCol":1,"startOffset":1,"endOffset":1}),
1008
+ ]
1009
+ );
1010
+ }
1011
+
1012
+ #[test]
1013
+ fn document_and_foreign_tree_context_control_tokenization() {
1014
+ assert!(collect(
1015
+ "<!doctype html><svg><![CDATA[x]]><path/></svg>",
1016
+ true,
1017
+ &json!({})
1018
+ )
1019
+ .is_empty());
1020
+ assert!(collect("<p>fragment</p>", false, &json!({})).is_empty());
1021
+ let errors = collect("<!doctype html><div/>", true, &json!({}));
1022
+ assert_eq!(
1023
+ errors,
1024
+ vec![
1025
+ json!({"code":"non-void-html-element-start-tag-with-trailing-solidus","startLine":1,"endLine":1,"startCol":16,"endCol":22,"startOffset":15,"endOffset":21})
1026
+ ]
1027
+ );
1028
+ }
1029
+
1030
+ #[test]
1031
+ fn numeric_entity_and_eof_errors_are_not_generic_recovery_warnings() {
1032
+ let errors = collect("<p>&#0;&#x110000;&amp x</p>", true, &json!({}));
1033
+ let codes: Vec<_> = errors
1034
+ .iter()
1035
+ .map(|error| error["code"].as_str().unwrap())
1036
+ .collect();
1037
+ assert_eq!(
1038
+ codes,
1039
+ vec![
1040
+ "missing-doctype",
1041
+ "null-character-reference",
1042
+ "character-reference-outside-unicode-range",
1043
+ "missing-semicolon-after-character-reference"
1044
+ ]
1045
+ );
1046
+ assert_eq!(errors[1]["startOffset"], 7);
1047
+ assert_eq!(errors[2]["startOffset"], 17);
1048
+ assert_eq!(errors[3]["startOffset"], 21);
1049
+ assert_eq!(collect("<!--x", true, &json!({}))[0]["startOffset"], 5);
1050
+ }
1051
+
1052
+ #[test]
1053
+ fn incremental_positions_preserve_unicode_and_split_crlf() {
1054
+ let source = "ASCII😀\r\n<div café=>終";
1055
+ let expected = DiagnosticContext::new(source);
1056
+ let incremental = DiagnosticContext::streaming(true);
1057
+ for chunk in ["ASCII", "😀\r", "\n<div caf", "é=>", "終"] {
1058
+ incremental.append_source(chunk);
1059
+ }
1060
+ assert_eq!(incremental.source_len(), source.len());
1061
+ assert_eq!(incremental.source_between(5, 9), "😀");
1062
+ for start in 0..=source.len() {
1063
+ assert_eq!(
1064
+ incremental.location(start, source.len()),
1065
+ expected.location(start, source.len())
1066
+ );
1067
+ }
1068
+ assert_eq!(incremental.location(11, 11)["startLine"], 2);
1069
+ assert_eq!(incremental.location(11, 11)["startCol"], 1);
1070
+ assert_eq!(incremental.location(11, 11)["startOffset"], 9);
1071
+ }
1072
+
1073
+ #[test]
1074
+ fn ascii_streams_do_not_allocate_positions_or_retain_unrequested_source() {
1075
+ let source = "x".repeat(1024 * 1024);
1076
+ let retained = DiagnosticContext::streaming(true);
1077
+ retained.append_source(&source);
1078
+ assert_eq!(retained.positions.borrow().checkpoints.len(), 1);
1079
+ assert_eq!(
1080
+ retained.location(source.len(), source.len())["startOffset"],
1081
+ source.len()
1082
+ );
1083
+ let untracked = DiagnosticContext::streaming(false);
1084
+ untracked.append_source(&source);
1085
+ assert_eq!(untracked.source_len(), source.len());
1086
+ assert!(untracked.source.borrow().is_empty());
1087
+ assert_eq!(untracked.positions.borrow().checkpoints.len(), 1);
1088
+ }
1089
+
1090
+ #[test]
1091
+ fn diagnostics_only_streams_keep_a_bounded_utf8_window_and_global_positions() {
1092
+ let context = DiagnosticContext::streaming_with_source_mode(false, true);
1093
+ let chunk = "line café😀\r\n".repeat(8192);
1094
+ context.append_source(&chunk);
1095
+ context.append_source("<p a=>");
1096
+ assert!(context.source.borrow().len() <= 64 * 1024);
1097
+ assert_eq!(context.source_len(), chunk.len() + 6);
1098
+ assert_eq!(
1099
+ context.source_between(chunk.len(), context.source_len()),
1100
+ "<p a=>"
1101
+ );
1102
+ assert_eq!(
1103
+ context.location(chunk.len(), chunk.len())["startLine"],
1104
+ 8193
1105
+ );
1106
+ assert_eq!(context.location(chunk.len(), chunk.len())["startCol"], 1);
1107
+ assert_eq!(
1108
+ context.location(chunk.len(), chunk.len())["startOffset"],
1109
+ chunk.encode_utf16().count()
1110
+ );
1111
+ }
1112
+
1113
+ #[test]
1114
+ fn leading_whitespace_error_location_survives_source_window_eviction() {
1115
+ struct Input<'a> {
1116
+ source: &'a str,
1117
+ cursor: usize,
1118
+ context: Rc<DiagnosticContext>,
1119
+ }
1120
+ impl html5gum::Reader for Input<'_> {
1121
+ type Error = std::convert::Infallible;
1122
+ fn read_byte(&mut self) -> Result<Option<u8>, Self::Error> {
1123
+ if self.cursor == self.source.len() {
1124
+ return Ok(None);
1125
+ }
1126
+ let byte = self.source.as_bytes()[self.cursor];
1127
+ self.context
1128
+ .append_source(&self.source[self.cursor..self.cursor + 1]);
1129
+ self.cursor += 1;
1130
+ Ok(Some(byte))
1131
+ }
1132
+ fn try_read_string(
1133
+ &mut self,
1134
+ value: &[u8],
1135
+ case_sensitive: bool,
1136
+ ) -> Result<bool, Self::Error> {
1137
+ let remaining = &self.source.as_bytes()[self.cursor..];
1138
+ if remaining.len() < value.len()
1139
+ || !remaining[..value.len()]
1140
+ .iter()
1141
+ .zip(value)
1142
+ .all(|(left, right)| {
1143
+ if case_sensitive {
1144
+ left == right
1145
+ } else {
1146
+ left.eq_ignore_ascii_case(right)
1147
+ }
1148
+ })
1149
+ {
1150
+ return Ok(false);
1151
+ }
1152
+ for _ in value {
1153
+ self.read_byte()?;
1154
+ }
1155
+ Ok(true)
1156
+ }
1157
+ }
1158
+ let source = format!("{}&#32;\r\nX", " ".repeat(70_000));
1159
+ let context = Rc::new(DiagnosticContext::streaming_with_source_mode(false, true));
1160
+ let reader = Input {
1161
+ source: &source,
1162
+ cursor: 0,
1163
+ context: context.clone(),
1164
+ };
1165
+ let mut errors = Vec::new();
1166
+ parse_reader_with_sink(
1167
+ reader,
1168
+ true,
1169
+ &json!({}),
1170
+ CompatibleSink::with_errors(Html::new_document()),
1171
+ context,
1172
+ |sink, context| {
1173
+ for message in sink.take_errors() {
1174
+ if let Some(error) = context.error(&message) {
1175
+ errors.push(error);
1176
+ }
1177
+ }
1178
+ },
1179
+ )
1180
+ .unwrap();
1181
+ assert_eq!(
1182
+ errors,
1183
+ vec![
1184
+ json!({"code":"missing-doctype","startLine":2,"startCol":1,"startOffset":70_007,"endLine":2,"endCol":1,"endOffset":70_007})
1185
+ ]
1186
+ );
1187
+ }
1188
+
1189
+ #[test]
1190
+ fn encoded_surrogates_report_live_errors_without_private_use_aliases() {
1191
+ let mut units: Vec<u16> = "<!doctype html>😀\r\n<p a=\"".encode_utf16().collect();
1192
+ units.extend([0xd800, 0xe000, 0xdfff]);
1193
+ units.extend("\">".encode_utf16());
1194
+ units.push(0xd800);
1195
+ units.extend("</p>".encode_utf16());
1196
+ let source = crate::codec::from_units(&units);
1197
+ let errors = collect(&source, true, &json!({"__utf16Codec":true}));
1198
+ assert_eq!(
1199
+ errors,
1200
+ vec![
1201
+ json!({"code":"surrogate-in-input-stream","startLine":2,"endLine":2,"startCol":7,"endCol":7,"startOffset":25,"endOffset":25}),
1202
+ json!({"code":"surrogate-in-input-stream","startLine":2,"endLine":2,"startCol":9,"endCol":9,"startOffset":27,"endOffset":27}),
1203
+ json!({"code":"surrogate-in-input-stream","startLine":2,"endLine":2,"startCol":12,"endCol":12,"startOffset":30,"endOffset":30})
1204
+ ]
1205
+ );
1206
+ }
1207
+
1208
+ #[test]
1209
+ fn numeric_private_use_entities_stay_literal_in_every_native_parser() {
1210
+ let source = "<!doctype html><p a=\"&#xe000;&#xe800;\">&#xe000;&#xe800;&#xd800;</p>";
1211
+ for options in [
1212
+ json!({"__utf16Codec":true}),
1213
+ json!({"__utf16Codec":true,"xmlMode":true}),
1214
+ json!({"__utf16Codec":true,"_useHtmlParser2":true,"xmlMode":false}),
1215
+ ] {
1216
+ let document =
1217
+ crate::NativeDocument::new(source.to_owned(), true, Some(options.to_string()));
1218
+ let text: Value = serde_json::from_str(
1219
+ &document
1220
+ .op("text".into(), json!({"ids":[0]}).to_string())
1221
+ .unwrap(),
1222
+ )
1223
+ .unwrap();
1224
+ assert_eq!(
1225
+ crate::codec::to_units(text.as_str().unwrap()),
1226
+ vec![0xe000, 0xe800, 0xfffd]
1227
+ );
1228
+ let ids = document.select("p".into(), None).unwrap();
1229
+ let attribute: Value = serde_json::from_str(
1230
+ &document
1231
+ .op("attr".into(), json!({"ids":ids,"name":"a"}).to_string())
1232
+ .unwrap(),
1233
+ )
1234
+ .unwrap();
1235
+ assert_eq!(
1236
+ crate::codec::to_units(attribute.as_str().unwrap()),
1237
+ vec![0xe000, 0xe800]
1238
+ );
1239
+ }
1240
+ let errors = collect(source, true, &json!({"__utf16Codec":true}));
1241
+ assert_eq!(
1242
+ errors
1243
+ .iter()
1244
+ .map(|error| error["code"].as_str().unwrap())
1245
+ .collect::<Vec<_>>(),
1246
+ vec!["surrogate-character-reference"]
1247
+ );
1248
+ }
1249
+ }