rusteerio 1.0.0 → 1.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 (65) hide show
  1. package/Cargo.lock +42 -1
  2. package/Cargo.toml +8 -1
  3. package/README.md +90 -51
  4. package/THIRD_PARTY_NOTICES.md +138 -2
  5. package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
  6. package/dist/chunk-FPRP6UIX.mjs.map +1 -0
  7. package/dist/index.cjs +2232 -750
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/slim.cjs +2226 -745
  13. package/dist/slim.cjs.map +1 -1
  14. package/dist/slim.d.mts +2 -2
  15. package/dist/slim.d.ts +2 -2
  16. package/dist/slim.mjs +1 -1
  17. package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
  18. package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
  19. package/dist/utils.d.mts +1 -1
  20. package/dist/utils.d.ts +1 -1
  21. package/native/src/arena.rs +231 -602
  22. package/native/src/attributes.rs +11 -5
  23. package/native/src/codec.rs +62 -18
  24. package/native/src/document_order.rs +711 -0
  25. package/native/src/extraction.rs +15 -3
  26. package/native/src/gc.rs +12 -3
  27. package/native/src/hook_sink.rs +77 -3
  28. package/native/src/id_hash.rs +42 -0
  29. package/native/src/immutable.rs +256 -0
  30. package/native/src/lib.rs +1097 -172
  31. package/native/src/markup_tokenizer.rs +668 -0
  32. package/native/src/runtime.rs +189 -33
  33. package/native/src/selector.rs +346 -44
  34. package/native/src/sink.rs +14 -1
  35. package/native/src/snapshot.rs +509 -0
  36. package/native/src/stream.rs +2 -2
  37. package/native/src/tokenizer.rs +377 -204
  38. package/native/vendor/README.md +3 -2
  39. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
  40. package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
  41. package/package.json +3 -1
  42. package/rusteerio.darwin-arm64.node +0 -0
  43. package/rusteerio.darwin-x64.node +0 -0
  44. package/rusteerio.linux-arm64-gnu.node +0 -0
  45. package/rusteerio.linux-arm64-musl.node +0 -0
  46. package/rusteerio.linux-x64-gnu.node +0 -0
  47. package/rusteerio.linux-x64-musl.node +0 -0
  48. package/rusteerio.win32-arm64-msvc.node +0 -0
  49. package/rusteerio.win32-x64-msvc.node +0 -0
  50. package/scripts/generate-notices.mjs +11 -0
  51. package/scripts/notices/htmlparser2-MIT.txt +18 -0
  52. package/scripts/notices/sources.json +6 -0
  53. package/src/attributes.ts +78 -44
  54. package/src/codec.ts +22 -3
  55. package/src/extract.ts +86 -2
  56. package/src/hooks.ts +148 -2
  57. package/src/index.ts +51 -40
  58. package/src/match.ts +207 -0
  59. package/src/native.ts +9 -1
  60. package/src/nodes.ts +531 -183
  61. package/src/router.ts +792 -229
  62. package/src/selection.ts +239 -125
  63. package/src/snapshot.ts +255 -0
  64. package/src/transport.ts +51 -42
  65. package/dist/chunk-DMRD3FGC.mjs.map +0 -1
@@ -1,6 +1,8 @@
1
- //! Consume events from an explicitly supplied JavaScript htmlparser2 Tokenizer.
2
- //! No markup is retokenized here: user event edits directly determine the DOM.
1
+ //! Build htmlparser2-compatible trees from tokenizer events. Events come from
2
+ //! the native tokenizer port or from an explicitly supplied JavaScript
3
+ //! Tokenizer, whose recorded events directly determine the DOM.
3
4
  use crate::arena::{append_parsed, html_parser_void, implied_close, DomArena, DomNode};
5
+ use crate::markup_tokenizer::Token;
4
6
  use serde_json::{json, Map, Value};
5
7
 
6
8
  struct Pending {
@@ -8,9 +10,9 @@ struct Pending {
8
10
  attrs: Map<String, Value>,
9
11
  start: usize,
10
12
  }
11
- struct Builder {
13
+ struct Builder<'a> {
12
14
  arena: DomArena,
13
- source: Vec<u16>,
15
+ source: &'a Source<'a>,
14
16
  parents: Vec<u32>,
15
17
  foreign: Vec<bool>,
16
18
  pending: Option<Pending>,
@@ -20,29 +22,57 @@ struct Builder {
20
22
  end: usize,
21
23
  xml: bool,
22
24
  lower_tags: bool,
25
+ lower_attrs: bool,
26
+ with_end_indices: bool,
27
+ recognize_self_closing: Option<bool>,
23
28
  }
24
- impl Builder {
29
+ impl<'a> Builder<'a> {
30
+ fn new(source: &'a Source<'a>, options: Value) -> Self {
31
+ let xml = crate::arena::xml_mode(&options);
32
+ let lower_tags = options
33
+ .get("lowerCaseTags")
34
+ .and_then(Value::as_bool)
35
+ .unwrap_or(!xml);
36
+ let lower_attrs = options
37
+ .get("lowerCaseAttributeNames")
38
+ .and_then(Value::as_bool)
39
+ .unwrap_or(!xml);
40
+ let with_end_indices = options.get("withEndIndices") == Some(&Value::Bool(true));
41
+ let recognize_self_closing = options.get("recognizeSelfClosing").and_then(Value::as_bool);
42
+ let mut arena = DomArena::from_records(vec![DomNode::new("root")], options);
43
+ // Growing the arena one doubling at a time copies every node record again.
44
+ arena.nodes.reserve(source.units.len() / 48);
45
+ Self {
46
+ arena,
47
+ source,
48
+ parents: vec![0],
49
+ foreign: vec![xml],
50
+ pending: None,
51
+ attribute_name: String::new(),
52
+ attribute_value: String::new(),
53
+ start: 0,
54
+ end: 0,
55
+ xml,
56
+ lower_tags,
57
+ lower_attrs,
58
+ with_end_indices,
59
+ recognize_self_closing,
60
+ }
61
+ }
25
62
  fn slice(&self, start: usize, end: usize) -> String {
26
- crate::codec::from_units(
27
- self.source
28
- .get(
29
- start.min(self.source.len())
30
- ..end.min(self.source.len()).max(start.min(self.source.len())),
31
- )
32
- .unwrap_or_default(),
33
- )
63
+ self.source.slice(start, end)
34
64
  }
35
65
  fn name(&self, start: usize, end: usize) -> String {
36
66
  let name = self.slice(start, end);
37
67
  if self.lower_tags {
38
- name.to_lowercase()
68
+ lowercase(name)
39
69
  } else {
40
70
  name
41
71
  }
42
72
  }
43
73
  fn close(&mut self) {
44
74
  if let Some(id) = (self.parents.len() > 1).then(|| self.parents.pop().unwrap()) {
45
- if self.arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
75
+ if self.with_end_indices {
46
76
  self.arena.nodes[id as usize]
47
77
  .extras
48
78
  .insert("endIndex".into(), json!(self.end));
@@ -61,7 +91,8 @@ impl Builder {
61
91
  self.close();
62
92
  }
63
93
  }
64
- if !(!self.xml && html_parser_void(&name)) {
94
+ // htmlparser2 tracks foreign content only in HTML mode.
95
+ if !self.xml && !html_parser_void(&name) {
65
96
  if matches!(name.as_str(), "svg" | "math") {
66
97
  self.foreign.push(true);
67
98
  } else if matches!(
@@ -87,12 +118,14 @@ impl Builder {
87
118
  fn end_open(&mut self, self_closing: bool) {
88
119
  if let Some(pending) = self.pending.take() {
89
120
  let is_void = !self.xml && html_parser_void(&pending.name);
90
- let kind = if !self.xml && matches!(pending.name.as_str(), "script" | "style") {
91
- pending.name.as_str()
92
- } else {
93
- "tag"
121
+ let kind = match pending.name.as_str() {
122
+ "script" if !self.xml => "script",
123
+ "style" if !self.xml => "style",
124
+ _ => "tag",
94
125
  };
95
126
  let mut node = DomNode::new(kind);
127
+ let lowercase_name = (pending.name.bytes().any(|byte| byte.is_ascii_uppercase()))
128
+ .then(|| pending.name.to_ascii_lowercase());
96
129
  node.name = Some(pending.name);
97
130
  node.attribs = pending.attrs;
98
131
  let id = append_parsed(
@@ -102,6 +135,17 @@ impl Builder {
102
135
  pending.start,
103
136
  self.end,
104
137
  );
138
+ let name = lowercase_name
139
+ .as_deref()
140
+ .or(self.arena.nodes[id as usize].name.as_deref())
141
+ .unwrap_or("");
142
+ let index = self.arena.name_index.get_or_insert_with(Default::default);
143
+ match index.get_mut(name) {
144
+ Some(ids) => ids.push(id),
145
+ None => {
146
+ index.insert(name.to_owned(), vec![id]);
147
+ }
148
+ }
105
149
  if !is_void && !self_closing {
106
150
  self.parents.push(id);
107
151
  }
@@ -145,34 +189,107 @@ impl Builder {
145
189
  }
146
190
  }
147
191
 
192
+ /// Sources from JavaScript, or that contain codec frames, use the lossless
193
+ /// UTF-16 representation; others are plain Unicode.
194
+ fn is_codec_encoded(options: &Value) -> bool {
195
+ options.get("__utf16Codec") == Some(&Value::Bool(true))
196
+ || options.get("__losslessUnicode") == Some(&Value::Bool(true))
197
+ }
198
+
199
+ fn lowercase(mut name: String) -> String {
200
+ if name.is_ascii() {
201
+ name.make_ascii_lowercase();
202
+ name
203
+ } else {
204
+ name.to_lowercase()
205
+ }
206
+ }
207
+
208
+ /// The source as UTF-16 units, with each unit's byte offset in the original
209
+ /// string so spans can be copied from it instead of re-encoded.
210
+ struct Source<'a> {
211
+ text: &'a str,
212
+ units: Vec<u16>,
213
+ /// Byte offset per unit index, plus one for the end; `u32::MAX` marks an
214
+ /// index inside a character. `None` for ASCII, where offsets are indices.
215
+ offsets: Option<Vec<u32>>,
216
+ codec: bool,
217
+ }
218
+
219
+ struct IndexedUnits {
220
+ units: Vec<u16>,
221
+ offsets: Vec<u32>,
222
+ }
223
+
224
+ impl crate::codec::UnitSink for IndexedUnits {
225
+ fn ascii(&mut self, start: usize, run: &[u8]) {
226
+ self.units.extend(run.iter().map(|value| u16::from(*value)));
227
+ self.offsets
228
+ .extend((start..start + run.len()).map(|offset| offset as u32));
229
+ }
230
+ fn unit(&mut self, offset: Option<usize>, unit: u16) {
231
+ self.units.push(unit);
232
+ self.offsets
233
+ .push(offset.map_or(u32::MAX, |offset| offset as u32));
234
+ }
235
+ }
236
+
237
+ impl<'a> Source<'a> {
238
+ fn new(text: &'a str, options: &Value) -> Self {
239
+ let codec = is_codec_encoded(options);
240
+ if text.is_ascii() {
241
+ return Self {
242
+ text,
243
+ units: text.bytes().map(u16::from).collect(),
244
+ offsets: None,
245
+ codec,
246
+ };
247
+ }
248
+ let mut indexed = IndexedUnits {
249
+ units: Vec::with_capacity(text.len()),
250
+ offsets: Vec::with_capacity(text.len() + 1),
251
+ };
252
+ let framed = codec && crate::codec::has_encoding(text);
253
+ crate::codec::visit_units(text, framed, &mut indexed);
254
+ indexed.offsets.push(text.len() as u32);
255
+ Self {
256
+ text,
257
+ units: indexed.units,
258
+ offsets: Some(indexed.offsets),
259
+ codec,
260
+ }
261
+ }
262
+ fn slice(&self, start: usize, end: usize) -> String {
263
+ let start = start.min(self.units.len());
264
+ let end = end.clamp(start, self.units.len());
265
+ let Some(offsets) = self.offsets.as_ref() else {
266
+ return self.text[start..end].to_owned();
267
+ };
268
+ match (offsets[start], offsets[end]) {
269
+ (from, to) if from != u32::MAX && to != u32::MAX => {
270
+ self.text[from as usize..to as usize].to_owned()
271
+ }
272
+ _ if self.codec => crate::codec::from_units(&self.units[start..end]),
273
+ _ => String::from_utf16_lossy(&self.units[start..end]),
274
+ }
275
+ }
276
+ }
277
+
278
+ /// Parses markup with the native port of htmlparser2's Tokenizer.
279
+ pub(crate) fn parse_markup(source: &str, options: Value) -> DomArena {
280
+ let source = Source::new(source, &options);
281
+ let decode = options.get("decodeEntities") != Some(&Value::Bool(false));
282
+ let mut builder = Builder::new(&source, options);
283
+ let xml = builder.xml;
284
+ crate::markup_tokenizer::tokenize(&source.units, xml, decode, |token| builder.handle(token));
285
+ builder.arena
286
+ }
287
+
288
+ /// Replays events recorded from a JavaScript htmlparser2 Tokenizer.
148
289
  pub(crate) fn parse(source: &str, events: &Value, options: Value) -> DomArena {
149
- let xml = crate::arena::xml_mode(&options);
150
- let utf16_codec = options.get("__utf16Codec") == Some(&Value::Bool(true));
151
- let lower_tags = options
152
- .get("lowerCaseTags")
153
- .and_then(Value::as_bool)
154
- .unwrap_or(!xml);
155
- let lower_attrs = options
156
- .get("lowerCaseAttributeNames")
157
- .and_then(Value::as_bool)
158
- .unwrap_or(!xml);
159
- let mut builder = Builder {
160
- arena: DomArena::from_records(vec![DomNode::new("root")], options),
161
- source: if utf16_codec {
162
- crate::codec::to_units(source)
163
- } else {
164
- source.encode_utf16().collect()
165
- },
166
- parents: vec![0],
167
- foreign: vec![xml],
168
- pending: None,
169
- attribute_name: String::new(),
170
- attribute_value: String::new(),
171
- start: 0,
172
- end: 0,
173
- xml,
174
- lower_tags,
175
- };
290
+ let source = Source::new(source, &options);
291
+ let units = source.units.len() as i64;
292
+ let mut builder = Builder::new(&source, options);
176
293
  for event in events.as_array().into_iter().flatten() {
177
294
  let Some(tuple) = event.as_array() else {
178
295
  continue;
@@ -180,176 +297,232 @@ pub(crate) fn parse(source: &str, events: &Value, options: Value) -> DomArena {
180
297
  let Some(event) = tuple.first().and_then(Value::as_str) else {
181
298
  continue;
182
299
  };
183
- let arg = |index: usize| tuple.get(index + 1).and_then(Value::as_u64).unwrap_or(0) as usize;
300
+ // htmlparser2 reports an unset section start as -1, which its
301
+ // `slice` resolves from the end of the input.
302
+ let arg = |index: usize| {
303
+ let value = tuple.get(index + 1).and_then(Value::as_i64).unwrap_or(0);
304
+ if value < 0 {
305
+ (units + value).max(0) as usize
306
+ } else {
307
+ value as usize
308
+ }
309
+ };
184
310
  let (a, b, c) = (arg(0), arg(1), arg(2));
185
311
  match event {
186
- "ontext" => {
187
- let text = builder.slice(a, b);
188
- builder.text(text, b);
189
- }
312
+ "ontext" => builder.on_text(a, b, false),
190
313
  "ontextentity" => {
191
- let text = if a <= 0xffff {
192
- crate::codec::from_units(&[a as u16])
193
- } else {
194
- crate::codec::encode_scalar(char::from_u32(a as u32).unwrap_or('\u{fffd}'))
195
- };
314
+ let text = builder.code_point(a);
196
315
  builder.text(text, b);
197
316
  }
198
- "onopentagname" => {
199
- builder.end = b;
200
- let name = builder.name(a, b);
201
- builder.open(name);
202
- }
203
- "onopentagend" => {
204
- builder.end = a;
205
- builder.end_open(false);
206
- builder.start = a + 1;
207
- }
208
- "onselfclosingtag" => {
209
- builder.end = a;
210
- let self_closing = builder
211
- .arena
212
- .options
213
- .get("recognizeSelfClosing")
214
- .and_then(Value::as_bool)
215
- .unwrap_or(xml)
216
- || *builder.foreign.last().unwrap_or(&xml);
217
- builder.end_open(self_closing);
218
- builder.start = a + 1;
219
- }
220
- "onattribname" => {
221
- builder.start = a;
222
- let name = builder.slice(a, b);
223
- builder.attribute_name = if lower_attrs {
224
- name.to_lowercase()
225
- } else {
226
- name
227
- };
228
- }
229
- "onattribdata" => {
230
- let data = builder.slice(a, b);
231
- builder.attribute_value.push_str(&data);
232
- }
317
+ "onopentagname" => builder.on_open_tag_name(a, b),
318
+ "onopentagend" => builder.on_open_tag_end(a),
319
+ "onselfclosingtag" => builder.on_self_closing_tag(a),
320
+ "onattribname" => builder.on_attribute_name(a, b),
321
+ "onattribdata" => builder.on_attribute_data(a, b, false),
233
322
  "onattribentity" => {
234
- builder.attribute_value.push_str(&if a <= 0xffff {
235
- crate::codec::from_units(&[a as u16])
236
- } else {
237
- crate::codec::encode_scalar(char::from_u32(a as u32).unwrap_or('\u{fffd}'))
238
- });
239
- }
240
- "onattribend" => {
241
- builder.end = b;
242
- if let Some(pending) = builder.pending.as_mut() {
243
- pending
244
- .attrs
245
- .entry(builder.attribute_name.clone())
246
- .or_insert_with(|| Value::String(builder.attribute_value.clone()));
247
- }
248
- builder.attribute_value.clear();
249
- }
250
- "onclosetag" => {
251
- builder.end = b;
252
- let name = builder.name(a, b);
253
- if !xml
254
- && matches!(
255
- name.as_str(),
256
- "svg"
257
- | "math"
258
- | "mi"
259
- | "mo"
260
- | "mn"
261
- | "ms"
262
- | "mtext"
263
- | "annotation-xml"
264
- | "foreignobject"
265
- | "desc"
266
- | "title"
267
- )
268
- {
269
- builder.foreign.pop();
270
- }
271
- if !(!xml && html_parser_void(&name)) {
272
- if let Some(position) = builder.parents.iter().rposition(|id| {
273
- builder
274
- .arena
275
- .node(*id)
276
- .and_then(|node| node.name.as_deref())
277
- == Some(name.as_str())
278
- }) {
279
- while builder.parents.len() > position {
280
- builder.close();
281
- }
282
- } else if !xml && name == "p" {
283
- builder.open(name);
284
- builder.end_open(true);
285
- }
286
- } else if !xml && name == "br" {
287
- builder.open(name);
288
- builder.end_open(true);
289
- }
290
- builder.start = b + 1;
323
+ let text = builder.code_point(a);
324
+ builder.attribute_value.push_str(&text);
291
325
  }
326
+ "onattribend" => builder.on_attribute_end(b),
327
+ "onclosetag" => builder.on_close_tag(a, b),
292
328
  "ondeclaration" => builder.instruction(a, b, '!'),
293
329
  "onprocessinginstruction" => builder.instruction(a, b, '?'),
294
- "oncomment" => {
295
- let mut node = DomNode::new("comment");
296
- node.data = Some(builder.slice(a, b.saturating_sub(c)));
297
- append_parsed(
298
- &mut builder.arena,
299
- *builder.parents.last().unwrap(),
300
- node,
301
- builder.start,
302
- b,
303
- );
304
- builder.end = b;
305
- builder.start = b + 1;
330
+ "oncomment" => builder.on_comment(a, b, c),
331
+ "oncdata" => builder.on_cdata(a, b, c),
332
+ "onend" => builder.on_end(),
333
+ _ => {}
334
+ }
335
+ }
336
+ builder.arena
337
+ }
338
+
339
+ impl Builder<'_> {
340
+ fn handle(&mut self, token: Token) {
341
+ match token {
342
+ Token::Text { start, end, decode } => self.on_text(start, end, decode),
343
+ Token::OpenTagName { start, end } => self.on_open_tag_name(start, end),
344
+ Token::OpenTagEnd { end } => self.on_open_tag_end(end),
345
+ Token::SelfClosingTag { end } => self.on_self_closing_tag(end),
346
+ Token::AttributeName { start, end } => self.on_attribute_name(start, end),
347
+ Token::AttributeData { start, end, decode } => {
348
+ self.on_attribute_data(start, end, decode)
306
349
  }
307
- "oncdata" => {
308
- let data = builder.slice(a, b.saturating_sub(c));
309
- if xml || builder.arena.options.get("recognizeCDATA") == Some(&Value::Bool(true)) {
310
- let id = append_parsed(
311
- &mut builder.arena,
312
- *builder.parents.last().unwrap(),
313
- DomNode::new("cdata"),
314
- builder.start,
315
- b,
316
- );
317
- let mut text = DomNode::new("text");
318
- text.data = Some(data);
319
- text.parent = Some(id);
320
- text.extras.insert("startIndex".into(), Value::Null);
321
- text.extras.insert(
322
- "endIndex".into(),
323
- if builder.arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
324
- json!(b)
325
- } else {
326
- Value::Null
327
- },
328
- );
329
- let child = builder.arena.push(text);
330
- builder.arena.nodes[id as usize].children.push(child);
331
- } else {
332
- let mut node = DomNode::new("comment");
333
- node.data = Some(format!("[CDATA[{data}]]"));
334
- append_parsed(
335
- &mut builder.arena,
336
- *builder.parents.last().unwrap(),
337
- node,
338
- builder.start,
339
- b,
340
- );
341
- }
342
- builder.end = b;
343
- builder.start = b + 1;
350
+ Token::AttributeEnd { end } => self.on_attribute_end(end),
351
+ Token::CloseTag { start, end } => self.on_close_tag(start, end),
352
+ Token::Comment {
353
+ start,
354
+ end,
355
+ end_offset,
356
+ } => self.on_comment(start, end, end_offset),
357
+ Token::Cdata {
358
+ start,
359
+ end,
360
+ end_offset,
361
+ } => self.on_cdata(start, end, end_offset),
362
+ Token::Declaration { start, end } => self.instruction(start, end, '!'),
363
+ Token::ProcessingInstruction { start, end } => self.instruction(start, end, '?'),
364
+ Token::End => self.on_end(),
365
+ }
366
+ }
367
+ fn code_point(&self, code_point: usize) -> String {
368
+ if code_point <= 0xffff {
369
+ crate::codec::from_units(&[code_point as u16])
370
+ } else {
371
+ crate::codec::encode_scalar(char::from_u32(code_point as u32).unwrap_or('\u{fffd}'))
372
+ }
373
+ }
374
+ fn on_text(&mut self, start: usize, end: usize, decode: bool) {
375
+ let raw = self.slice(start, end);
376
+ let text = if decode && raw.contains('&') {
377
+ crate::arena::decode_entities_mode(&raw, self.xml, false, self.source.codec)
378
+ } else {
379
+ raw
380
+ };
381
+ self.text(text, end);
382
+ }
383
+ fn on_open_tag_name(&mut self, start: usize, end: usize) {
384
+ self.end = end;
385
+ let name = self.name(start, end);
386
+ self.open(name);
387
+ }
388
+ fn on_open_tag_end(&mut self, end: usize) {
389
+ self.end = end;
390
+ self.end_open(false);
391
+ self.start = end + 1;
392
+ }
393
+ fn on_self_closing_tag(&mut self, end: usize) {
394
+ self.end = end;
395
+ let self_closing = self.recognize_self_closing.unwrap_or(self.xml)
396
+ || *self.foreign.last().unwrap_or(&self.xml);
397
+ self.end_open(self_closing);
398
+ self.start = end + 1;
399
+ }
400
+ fn on_attribute_name(&mut self, start: usize, end: usize) {
401
+ self.start = start;
402
+ let name = self.slice(start, end);
403
+ self.attribute_name = if self.lower_attrs {
404
+ lowercase(name)
405
+ } else {
406
+ name
407
+ };
408
+ }
409
+ fn on_attribute_data(&mut self, start: usize, end: usize, decode: bool) {
410
+ let raw = self.slice(start, end);
411
+ let data = if decode && raw.contains('&') {
412
+ crate::arena::decode_entities_mode(&raw, self.xml, true, self.source.codec)
413
+ } else {
414
+ raw
415
+ };
416
+ if self.attribute_value.is_empty() {
417
+ self.attribute_value = data;
418
+ } else {
419
+ self.attribute_value.push_str(&data);
420
+ }
421
+ }
422
+ fn on_attribute_end(&mut self, end: usize) {
423
+ self.end = end;
424
+ let value = std::mem::take(&mut self.attribute_value);
425
+ if let Some(pending) = self.pending.as_mut() {
426
+ if !pending.attrs.contains_key(&self.attribute_name) {
427
+ pending
428
+ .attrs
429
+ .insert(self.attribute_name.clone(), Value::String(value));
344
430
  }
345
- "onend" => {
346
- builder.end = builder.start;
347
- while builder.parents.len() > 1 {
348
- builder.close();
431
+ }
432
+ }
433
+ fn on_close_tag(&mut self, start: usize, end: usize) {
434
+ self.end = end;
435
+ let name = self.name(start, end);
436
+ if !self.xml
437
+ && matches!(
438
+ name.as_str(),
439
+ "svg"
440
+ | "math"
441
+ | "mi"
442
+ | "mo"
443
+ | "mn"
444
+ | "ms"
445
+ | "mtext"
446
+ | "annotation-xml"
447
+ | "foreignobject"
448
+ | "desc"
449
+ | "title"
450
+ )
451
+ {
452
+ self.foreign.pop();
453
+ }
454
+ if !(!self.xml && html_parser_void(&name)) {
455
+ if let Some(position) = self.parents.iter().rposition(|id| {
456
+ self.arena.node(*id).and_then(|node| node.name.as_deref()) == Some(name.as_str())
457
+ }) {
458
+ while self.parents.len() > position {
459
+ self.close();
349
460
  }
461
+ } else if !self.xml && name == "p" {
462
+ self.open(name);
463
+ self.end_open(true);
350
464
  }
351
- _ => {}
465
+ } else if !self.xml && name == "br" {
466
+ self.open(name);
467
+ self.end_open(true);
468
+ }
469
+ self.start = end + 1;
470
+ }
471
+ fn on_comment(&mut self, start: usize, end: usize, end_offset: usize) {
472
+ let mut node = DomNode::new("comment");
473
+ node.data = Some(self.slice(start, end.saturating_sub(end_offset)));
474
+ append_parsed(
475
+ &mut self.arena,
476
+ *self.parents.last().unwrap(),
477
+ node,
478
+ self.start,
479
+ end,
480
+ );
481
+ self.end = end;
482
+ self.start = end + 1;
483
+ }
484
+ fn on_cdata(&mut self, start: usize, end: usize, end_offset: usize) {
485
+ let data = self.slice(start, end.saturating_sub(end_offset));
486
+ if self.xml || self.arena.options.get("recognizeCDATA") == Some(&Value::Bool(true)) {
487
+ let id = append_parsed(
488
+ &mut self.arena,
489
+ *self.parents.last().unwrap(),
490
+ DomNode::new("cdata"),
491
+ self.start,
492
+ end,
493
+ );
494
+ let mut text = DomNode::new("text");
495
+ text.data = Some(data);
496
+ text.parent = Some(id);
497
+ text.extras.insert("startIndex".into(), Value::Null);
498
+ text.extras.insert(
499
+ "endIndex".into(),
500
+ if self.arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
501
+ json!(end)
502
+ } else {
503
+ Value::Null
504
+ },
505
+ );
506
+ let child = self.arena.push(text);
507
+ self.arena.nodes[id as usize].children.push(child);
508
+ } else {
509
+ let mut node = DomNode::new("comment");
510
+ node.data = Some(format!("[CDATA[{data}]]"));
511
+ append_parsed(
512
+ &mut self.arena,
513
+ *self.parents.last().unwrap(),
514
+ node,
515
+ self.start,
516
+ end,
517
+ );
518
+ }
519
+ self.end = end;
520
+ self.start = end + 1;
521
+ }
522
+ fn on_end(&mut self) {
523
+ self.end = self.start;
524
+ while self.parents.len() > 1 {
525
+ self.close();
352
526
  }
353
527
  }
354
- builder.arena
355
528
  }
@@ -13,8 +13,9 @@ observe the same construction process used by the Rust HTML5 tree builder.
13
13
  Ordinary parsing uses the default observer methods.
14
14
 
15
15
  An opt-in tokenizer flag also escapes private-use characters produced by
16
- character references. This preserves the distinction between literal Unicode
17
- and Rusteerio's internal representation of lone JavaScript UTF-16 surrogates.
16
+ character references, emitting each escape and its character as one text token.
17
+ This preserves the distinction between literal Unicode and Rusteerio's internal
18
+ representation of lone JavaScript UTF-16 surrogates.
18
19
  The flag is disabled for upstream users of these crates. The original HTML5
19
20
  character-reference decoding rules remain in place.
20
21