rusteerio 1.0.0 → 1.0.1

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.
@@ -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,22 +22,41 @@ struct Builder {
20
22
  end: usize,
21
23
  xml: bool,
22
24
  lower_tags: bool,
25
+ lower_attrs: bool,
23
26
  }
24
- impl Builder {
27
+ impl<'a> Builder<'a> {
28
+ fn new(source: &'a Source<'a>, options: Value) -> Self {
29
+ let xml = crate::arena::xml_mode(&options);
30
+ let lower_tags = options
31
+ .get("lowerCaseTags")
32
+ .and_then(Value::as_bool)
33
+ .unwrap_or(!xml);
34
+ let lower_attrs = options
35
+ .get("lowerCaseAttributeNames")
36
+ .and_then(Value::as_bool)
37
+ .unwrap_or(!xml);
38
+ Self {
39
+ arena: DomArena::from_records(vec![DomNode::new("root")], options),
40
+ source,
41
+ parents: vec![0],
42
+ foreign: vec![xml],
43
+ pending: None,
44
+ attribute_name: String::new(),
45
+ attribute_value: String::new(),
46
+ start: 0,
47
+ end: 0,
48
+ xml,
49
+ lower_tags,
50
+ lower_attrs,
51
+ }
52
+ }
25
53
  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
- )
54
+ self.source.slice(start, end)
34
55
  }
35
56
  fn name(&self, start: usize, end: usize) -> String {
36
57
  let name = self.slice(start, end);
37
58
  if self.lower_tags {
38
- name.to_lowercase()
59
+ lowercase(name)
39
60
  } else {
40
61
  name
41
62
  }
@@ -61,7 +82,8 @@ impl Builder {
61
82
  self.close();
62
83
  }
63
84
  }
64
- if !(!self.xml && html_parser_void(&name)) {
85
+ // htmlparser2 tracks foreign content only in HTML mode.
86
+ if !self.xml && !html_parser_void(&name) {
65
87
  if matches!(name.as_str(), "svg" | "math") {
66
88
  self.foreign.push(true);
67
89
  } else if matches!(
@@ -145,34 +167,107 @@ impl Builder {
145
167
  }
146
168
  }
147
169
 
170
+ /// Sources from JavaScript, or that contain codec frames, use the lossless
171
+ /// UTF-16 representation; others are plain Unicode.
172
+ fn is_codec_encoded(options: &Value) -> bool {
173
+ options.get("__utf16Codec") == Some(&Value::Bool(true))
174
+ || options.get("__losslessUnicode") == Some(&Value::Bool(true))
175
+ }
176
+
177
+ fn lowercase(mut name: String) -> String {
178
+ if name.is_ascii() {
179
+ name.make_ascii_lowercase();
180
+ name
181
+ } else {
182
+ name.to_lowercase()
183
+ }
184
+ }
185
+
186
+ /// The source as UTF-16 units, with each unit's byte offset in the original
187
+ /// string so spans can be copied from it instead of re-encoded.
188
+ struct Source<'a> {
189
+ text: &'a str,
190
+ units: Vec<u16>,
191
+ /// Byte offset per unit index, plus one for the end; `u32::MAX` marks an
192
+ /// index inside a character. `None` for ASCII, where offsets are indices.
193
+ offsets: Option<Vec<u32>>,
194
+ codec: bool,
195
+ }
196
+
197
+ struct IndexedUnits {
198
+ units: Vec<u16>,
199
+ offsets: Vec<u32>,
200
+ }
201
+
202
+ impl crate::codec::UnitSink for IndexedUnits {
203
+ fn ascii(&mut self, start: usize, run: &[u8]) {
204
+ self.units.extend(run.iter().map(|value| u16::from(*value)));
205
+ self.offsets
206
+ .extend((start..start + run.len()).map(|offset| offset as u32));
207
+ }
208
+ fn unit(&mut self, offset: Option<usize>, unit: u16) {
209
+ self.units.push(unit);
210
+ self.offsets
211
+ .push(offset.map_or(u32::MAX, |offset| offset as u32));
212
+ }
213
+ }
214
+
215
+ impl<'a> Source<'a> {
216
+ fn new(text: &'a str, options: &Value) -> Self {
217
+ let codec = is_codec_encoded(options);
218
+ if text.is_ascii() {
219
+ return Self {
220
+ text,
221
+ units: text.bytes().map(u16::from).collect(),
222
+ offsets: None,
223
+ codec,
224
+ };
225
+ }
226
+ let mut indexed = IndexedUnits {
227
+ units: Vec::with_capacity(text.len()),
228
+ offsets: Vec::with_capacity(text.len() + 1),
229
+ };
230
+ let framed = codec && crate::codec::has_encoding(text);
231
+ crate::codec::visit_units(text, framed, &mut indexed);
232
+ indexed.offsets.push(text.len() as u32);
233
+ Self {
234
+ text,
235
+ units: indexed.units,
236
+ offsets: Some(indexed.offsets),
237
+ codec,
238
+ }
239
+ }
240
+ fn slice(&self, start: usize, end: usize) -> String {
241
+ let start = start.min(self.units.len());
242
+ let end = end.clamp(start, self.units.len());
243
+ let Some(offsets) = self.offsets.as_ref() else {
244
+ return self.text[start..end].to_owned();
245
+ };
246
+ match (offsets[start], offsets[end]) {
247
+ (from, to) if from != u32::MAX && to != u32::MAX => {
248
+ self.text[from as usize..to as usize].to_owned()
249
+ }
250
+ _ if self.codec => crate::codec::from_units(&self.units[start..end]),
251
+ _ => String::from_utf16_lossy(&self.units[start..end]),
252
+ }
253
+ }
254
+ }
255
+
256
+ /// Parses markup with the native port of htmlparser2's Tokenizer.
257
+ pub(crate) fn parse_markup(source: &str, options: Value) -> DomArena {
258
+ let source = Source::new(source, &options);
259
+ let decode = options.get("decodeEntities") != Some(&Value::Bool(false));
260
+ let mut builder = Builder::new(&source, options);
261
+ let xml = builder.xml;
262
+ crate::markup_tokenizer::tokenize(&source.units, xml, decode, |token| builder.handle(token));
263
+ builder.arena
264
+ }
265
+
266
+ /// Replays events recorded from a JavaScript htmlparser2 Tokenizer.
148
267
  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
- };
268
+ let source = Source::new(source, &options);
269
+ let units = source.units.len() as i64;
270
+ let mut builder = Builder::new(&source, options);
176
271
  for event in events.as_array().into_iter().flatten() {
177
272
  let Some(tuple) = event.as_array() else {
178
273
  continue;
@@ -180,176 +275,237 @@ pub(crate) fn parse(source: &str, events: &Value, options: Value) -> DomArena {
180
275
  let Some(event) = tuple.first().and_then(Value::as_str) else {
181
276
  continue;
182
277
  };
183
- let arg = |index: usize| tuple.get(index + 1).and_then(Value::as_u64).unwrap_or(0) as usize;
278
+ // htmlparser2 reports an unset section start as -1, which its
279
+ // `slice` resolves from the end of the input.
280
+ let arg = |index: usize| {
281
+ let value = tuple.get(index + 1).and_then(Value::as_i64).unwrap_or(0);
282
+ if value < 0 {
283
+ (units + value).max(0) as usize
284
+ } else {
285
+ value as usize
286
+ }
287
+ };
184
288
  let (a, b, c) = (arg(0), arg(1), arg(2));
185
289
  match event {
186
- "ontext" => {
187
- let text = builder.slice(a, b);
188
- builder.text(text, b);
189
- }
290
+ "ontext" => builder.on_text(a, b, false),
190
291
  "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
- };
292
+ let text = builder.code_point(a);
196
293
  builder.text(text, b);
197
294
  }
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
- }
295
+ "onopentagname" => builder.on_open_tag_name(a, b),
296
+ "onopentagend" => builder.on_open_tag_end(a),
297
+ "onselfclosingtag" => builder.on_self_closing_tag(a),
298
+ "onattribname" => builder.on_attribute_name(a, b),
299
+ "onattribdata" => builder.on_attribute_data(a, b, false),
233
300
  "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;
301
+ let text = builder.code_point(a);
302
+ builder.attribute_value.push_str(&text);
291
303
  }
304
+ "onattribend" => builder.on_attribute_end(b),
305
+ "onclosetag" => builder.on_close_tag(a, b),
292
306
  "ondeclaration" => builder.instruction(a, b, '!'),
293
307
  "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;
308
+ "oncomment" => builder.on_comment(a, b, c),
309
+ "oncdata" => builder.on_cdata(a, b, c),
310
+ "onend" => builder.on_end(),
311
+ _ => {}
312
+ }
313
+ }
314
+ builder.arena
315
+ }
316
+
317
+ impl Builder<'_> {
318
+ fn handle(&mut self, token: Token) {
319
+ match token {
320
+ Token::Text { start, end, decode } => self.on_text(start, end, decode),
321
+ Token::OpenTagName { start, end } => self.on_open_tag_name(start, end),
322
+ Token::OpenTagEnd { end } => self.on_open_tag_end(end),
323
+ Token::SelfClosingTag { end } => self.on_self_closing_tag(end),
324
+ Token::AttributeName { start, end } => self.on_attribute_name(start, end),
325
+ Token::AttributeData { start, end, decode } => {
326
+ self.on_attribute_data(start, end, decode)
306
327
  }
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;
328
+ Token::AttributeEnd { end } => self.on_attribute_end(end),
329
+ Token::CloseTag { start, end } => self.on_close_tag(start, end),
330
+ Token::Comment {
331
+ start,
332
+ end,
333
+ end_offset,
334
+ } => self.on_comment(start, end, end_offset),
335
+ Token::Cdata {
336
+ start,
337
+ end,
338
+ end_offset,
339
+ } => self.on_cdata(start, end, end_offset),
340
+ Token::Declaration { start, end } => self.instruction(start, end, '!'),
341
+ Token::ProcessingInstruction { start, end } => self.instruction(start, end, '?'),
342
+ Token::End => self.on_end(),
343
+ }
344
+ }
345
+ fn code_point(&self, code_point: usize) -> String {
346
+ if code_point <= 0xffff {
347
+ crate::codec::from_units(&[code_point as u16])
348
+ } else {
349
+ crate::codec::encode_scalar(char::from_u32(code_point as u32).unwrap_or('\u{fffd}'))
350
+ }
351
+ }
352
+ fn on_text(&mut self, start: usize, end: usize, decode: bool) {
353
+ let raw = self.slice(start, end);
354
+ let text = if decode && raw.contains('&') {
355
+ crate::arena::decode_entities_mode(&raw, self.xml, false, self.source.codec)
356
+ } else {
357
+ raw
358
+ };
359
+ self.text(text, end);
360
+ }
361
+ fn on_open_tag_name(&mut self, start: usize, end: usize) {
362
+ self.end = end;
363
+ let name = self.name(start, end);
364
+ self.open(name);
365
+ }
366
+ fn on_open_tag_end(&mut self, end: usize) {
367
+ self.end = end;
368
+ self.end_open(false);
369
+ self.start = end + 1;
370
+ }
371
+ fn on_self_closing_tag(&mut self, end: usize) {
372
+ self.end = end;
373
+ let self_closing = self
374
+ .arena
375
+ .options
376
+ .get("recognizeSelfClosing")
377
+ .and_then(Value::as_bool)
378
+ .unwrap_or(self.xml)
379
+ || *self.foreign.last().unwrap_or(&self.xml);
380
+ self.end_open(self_closing);
381
+ self.start = end + 1;
382
+ }
383
+ fn on_attribute_name(&mut self, start: usize, end: usize) {
384
+ self.start = start;
385
+ let name = self.slice(start, end);
386
+ self.attribute_name = if self.lower_attrs {
387
+ lowercase(name)
388
+ } else {
389
+ name
390
+ };
391
+ }
392
+ fn on_attribute_data(&mut self, start: usize, end: usize, decode: bool) {
393
+ let raw = self.slice(start, end);
394
+ let data = if decode && raw.contains('&') {
395
+ crate::arena::decode_entities_mode(&raw, self.xml, true, self.source.codec)
396
+ } else {
397
+ raw
398
+ };
399
+ if self.attribute_value.is_empty() {
400
+ self.attribute_value = data;
401
+ } else {
402
+ self.attribute_value.push_str(&data);
403
+ }
404
+ }
405
+ fn on_attribute_end(&mut self, end: usize) {
406
+ self.end = end;
407
+ let value = std::mem::take(&mut self.attribute_value);
408
+ if let Some(pending) = self.pending.as_mut() {
409
+ if !pending.attrs.contains_key(&self.attribute_name) {
410
+ pending
411
+ .attrs
412
+ .insert(self.attribute_name.clone(), Value::String(value));
344
413
  }
345
- "onend" => {
346
- builder.end = builder.start;
347
- while builder.parents.len() > 1 {
348
- builder.close();
414
+ }
415
+ }
416
+ fn on_close_tag(&mut self, start: usize, end: usize) {
417
+ self.end = end;
418
+ let name = self.name(start, end);
419
+ if !self.xml
420
+ && matches!(
421
+ name.as_str(),
422
+ "svg"
423
+ | "math"
424
+ | "mi"
425
+ | "mo"
426
+ | "mn"
427
+ | "ms"
428
+ | "mtext"
429
+ | "annotation-xml"
430
+ | "foreignobject"
431
+ | "desc"
432
+ | "title"
433
+ )
434
+ {
435
+ self.foreign.pop();
436
+ }
437
+ if !(!self.xml && html_parser_void(&name)) {
438
+ if let Some(position) = self.parents.iter().rposition(|id| {
439
+ self.arena.node(*id).and_then(|node| node.name.as_deref()) == Some(name.as_str())
440
+ }) {
441
+ while self.parents.len() > position {
442
+ self.close();
349
443
  }
444
+ } else if !self.xml && name == "p" {
445
+ self.open(name);
446
+ self.end_open(true);
350
447
  }
351
- _ => {}
448
+ } else if !self.xml && name == "br" {
449
+ self.open(name);
450
+ self.end_open(true);
451
+ }
452
+ self.start = end + 1;
453
+ }
454
+ fn on_comment(&mut self, start: usize, end: usize, end_offset: usize) {
455
+ let mut node = DomNode::new("comment");
456
+ node.data = Some(self.slice(start, end.saturating_sub(end_offset)));
457
+ append_parsed(
458
+ &mut self.arena,
459
+ *self.parents.last().unwrap(),
460
+ node,
461
+ self.start,
462
+ end,
463
+ );
464
+ self.end = end;
465
+ self.start = end + 1;
466
+ }
467
+ fn on_cdata(&mut self, start: usize, end: usize, end_offset: usize) {
468
+ let data = self.slice(start, end.saturating_sub(end_offset));
469
+ if self.xml || self.arena.options.get("recognizeCDATA") == Some(&Value::Bool(true)) {
470
+ let id = append_parsed(
471
+ &mut self.arena,
472
+ *self.parents.last().unwrap(),
473
+ DomNode::new("cdata"),
474
+ self.start,
475
+ end,
476
+ );
477
+ let mut text = DomNode::new("text");
478
+ text.data = Some(data);
479
+ text.parent = Some(id);
480
+ text.extras.insert("startIndex".into(), Value::Null);
481
+ text.extras.insert(
482
+ "endIndex".into(),
483
+ if self.arena.options.get("withEndIndices") == Some(&Value::Bool(true)) {
484
+ json!(end)
485
+ } else {
486
+ Value::Null
487
+ },
488
+ );
489
+ let child = self.arena.push(text);
490
+ self.arena.nodes[id as usize].children.push(child);
491
+ } else {
492
+ let mut node = DomNode::new("comment");
493
+ node.data = Some(format!("[CDATA[{data}]]"));
494
+ append_parsed(
495
+ &mut self.arena,
496
+ *self.parents.last().unwrap(),
497
+ node,
498
+ self.start,
499
+ end,
500
+ );
501
+ }
502
+ self.end = end;
503
+ self.start = end + 1;
504
+ }
505
+ fn on_end(&mut self) {
506
+ self.end = self.start;
507
+ while self.parents.len() > 1 {
508
+ self.close();
352
509
  }
353
510
  }
354
- builder.arena
355
511
  }
@@ -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
 
@@ -1698,19 +1698,29 @@ impl<Sink: TokenSink> Tokenizer<Sink> {
1698
1698
 
1699
1699
  for i in 0..num_chars {
1700
1700
  let c = chars[i as usize];
1701
- let prefix = (self.opts.rusteerio_codec && ('\u{e000}'..='\u{e800}').contains(&c))
1702
- .then_some('\u{e800}');
1703
- for c in prefix.into_iter().chain(std::iter::once(c)) {
1704
- match self.state.get() {
1705
- states::Data | states::RawData(states::Rcdata) => self.emit_char(c),
1706
-
1707
- states::AttributeValue(_) => go!(self: push_value c),
1708
-
1709
- _ => panic!(
1710
- "state {:?} should not be reachable in process_char_ref",
1711
- self.state.get()
1712
- ),
1713
- }
1701
+ let is_escaped =
1702
+ self.opts.rusteerio_codec && ('\u{e000}'..='\u{e800}').contains(&c);
1703
+ match self.state.get() {
1704
+ // The escape and its character form one text token, so a sink
1705
+ // that forwards each token never receives them separately.
1706
+ states::Data | states::RawData(states::Rcdata) if is_escaped => {
1707
+ let mut text = StrTendril::from_char('\u{e800}');
1708
+ text.push_char(c);
1709
+ self.emit_chars(text);
1710
+ },
1711
+ states::Data | states::RawData(states::Rcdata) => self.emit_char(c),
1712
+
1713
+ states::AttributeValue(_) => {
1714
+ if is_escaped {
1715
+ go!(self: push_value '\u{e800}');
1716
+ }
1717
+ go!(self: push_value c);
1718
+ },
1719
+
1720
+ _ => panic!(
1721
+ "state {:?} should not be reachable in process_char_ref",
1722
+ self.state.get()
1723
+ ),
1714
1724
  }
1715
1725
  }
1716
1726
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rusteerio",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Cheerio-compatible HTML processing with a Rust engine and parallel, non-blocking extraction",
5
5
  "license": "MIT",
6
6
  "repository": {