1use std::borrow::Cow;
20
21use memchr::memchr;
22
23use crate::error::Result;
24use crate::options::ParseOptions;
25use crate::xml_bytes_reader::{
26 BytesAttrs, BytesCData, BytesComment, BytesEndTag, BytesEvent, BytesPi, BytesStartTag,
27 BytesText, XmlBytesReader, XmlDeclInfo,
28};
29
30#[derive(Debug)]
34pub struct Attr<'src> {
35 pub name: &'src str,
38 pub value: Cow<'src, str>,
41}
42
43impl<'src> Attr<'src> {
44 pub fn name(&self) -> &'src str { self.name }
46 pub fn value(&self) -> &str { &self.value }
48}
49
50pub struct Attrs<'r, 'src> {
63 inner: BytesAttrs<'r, 'src>,
64}
65
66impl<'src> Iterator for Attrs<'_, 'src> {
67 type Item = Result<Attr<'src>>;
68
69 fn next(&mut self) -> Option<Self::Item> {
70 self.inner.next().map(|res| res.map(|ba| Attr {
71 name: unsafe { std::str::from_utf8_unchecked(ba.name) },
73 value: cow_bytes_to_str(ba.value),
74 }))
75 }
76}
77
78pub struct StartTag<'r, 'src> {
83 inner: BytesStartTag<'r, 'src>,
84}
85
86impl<'r, 'src> StartTag<'r, 'src> {
87 #[inline]
94 pub fn name(&self) -> &str {
95 unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
97 }
98
99 pub fn name_cow(&self) -> Cow<'src, str> {
103 match self.inner.name_cow() {
104 Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
106 Cow::Owned(v) => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
107 }
108 }
109
110 pub fn attrs(self) -> Attrs<'r, 'src> {
112 Attrs { inner: self.inner.attrs() }
113 }
114
115 #[inline]
118 pub fn attrs_str(&self) -> &'src str {
119 unsafe { std::str::from_utf8_unchecked(self.inner.attrs_bytes()) }
121 }
122}
123
124impl std::fmt::Debug for StartTag<'_, '_> {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct("StartTag")
127 .field("name", &self.name())
128 .field("attrs", &self.attrs_str())
129 .finish()
130 }
131}
132
133pub struct EndTag<'src> {
136 inner: BytesEndTag<'src>,
137}
138
139impl<'src> EndTag<'src> {
140 #[inline]
141 pub fn name(&self) -> &'src str {
142 unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
144 }
145}
146
147impl std::fmt::Debug for EndTag<'_> {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("EndTag").field("name", &self.name()).finish()
150 }
151}
152
153pub struct Text<'src> { inner: BytesText<'src> }
155impl<'src> Text<'src> {
156 #[inline] pub fn as_str(&self) -> &str {
157 unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
159 }
160 pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
161}
162impl std::fmt::Debug for Text<'_> {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 write!(f, "Text({:?})", self.as_str())
165 }
166}
167
168pub struct CData<'src> { inner: BytesCData<'src> }
170impl<'src> CData<'src> {
171 #[inline] pub fn as_str(&self) -> &str {
172 unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
173 }
174 pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
175}
176impl std::fmt::Debug for CData<'_> {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 write!(f, "CData({:?})", self.as_str())
179 }
180}
181
182pub struct Comment<'src> { inner: BytesComment<'src> }
185impl<'src> Comment<'src> {
186 #[inline] pub fn as_str(&self) -> &str {
187 unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
188 }
189 pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
190}
191impl std::fmt::Debug for Comment<'_> {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 write!(f, "Comment({:?})", self.as_str())
194 }
195}
196
197pub struct Pi<'src> { inner: BytesPi<'src> }
199impl<'src> Pi<'src> {
200 #[inline] pub fn target(&self) -> &str {
201 unsafe { std::str::from_utf8_unchecked(self.inner.target()) }
202 }
203 #[inline] pub fn content(&self) -> &str {
204 unsafe { std::str::from_utf8_unchecked(self.inner.content()) }
205 }
206 pub fn into_parts(self) -> (Cow<'src, str>, Cow<'src, str>) {
207 let (t, c) = self.inner.into_parts();
208 (cow_bytes_to_str(t), cow_bytes_to_str(c))
209 }
210}
211impl std::fmt::Debug for Pi<'_> {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("Pi")
214 .field("target", &self.target())
215 .field("content", &self.content())
216 .finish()
217 }
218}
219
220#[derive(Debug)]
227pub enum Event<'r, 'src> {
228 StartElement(StartTag<'r, 'src>),
230 EndElement(EndTag<'src>),
233 Text(Text<'src>),
235 CData(CData<'src>),
237 Comment(Comment<'src>),
239 Pi(Pi<'src>),
241 EntityRef(EntityRef<'src>),
245 Eof,
247}
248
249pub struct EntityRef<'src> {
252 inner: crate::xml_bytes_reader::BytesEntityRef<'src>,
253}
254
255impl<'src> EntityRef<'src> {
256 #[inline]
259 pub fn name(&self) -> &'src str {
260 unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
262 }
263}
264
265impl std::fmt::Debug for EntityRef<'_> {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 f.debug_struct("EntityRef")
268 .field("name", &self.name())
269 .finish()
270 }
271}
272
273#[derive(Debug)]
281pub enum EventInto<'src> {
282 StartElement { name: Cow<'src, str> },
285 EndElement { name: Cow<'src, str> },
288 Text(Cow<'src, str>),
289 CData(Cow<'src, str>),
290 Comment(Cow<'src, str>),
291 Pi { target: Cow<'src, str>, content: Cow<'src, str> },
292 EntityRef { name: Cow<'src, str> },
295 Eof,
296}
297
298pub struct XmlReader<'src> {
306 inner: XmlBytesReader<'src>,
307}
308
309impl<'src> XmlReader<'src> {
310 #[allow(clippy::should_implement_trait)] pub fn from_str(input: &'src str) -> Self {
314 Self { inner: XmlBytesReader::from_str(input) }
315 }
316
317 pub fn from_bytes(src: &'src [u8]) -> Result<Self> {
320 XmlBytesReader::from_bytes(src).map(|inner| Self { inner })
321 }
322
323 pub unsafe fn from_bytes_unchecked(src: &'src [u8]) -> Self {
333 Self { inner: unsafe { XmlBytesReader::from_bytes_unchecked(src) } }
334 }
335
336 pub unsafe fn from_bytes_in_place_unchecked(src: &'src mut [u8]) -> Self {
344 Self { inner: unsafe { XmlBytesReader::from_bytes_in_place_unchecked(src) } }
345 }
346
347 pub fn with_options(self, opts: ParseOptions) -> Self {
350 Self { inner: self.inner.with_options(opts) }
351 }
352
353 pub fn xml_decl(&self) -> Option<&XmlDeclInfo> {
358 self.inner.xml_decl()
359 }
360
361 pub fn recovered_errors(&self) -> &[crate::error::XmlError] {
366 self.inner.recovered_errors()
367 }
368
369 #[inline]
380 pub fn src_offset(&self) -> usize {
381 self.inner.src_offset()
382 }
383
384 #[inline]
390 pub fn last_start_offset(&self) -> Option<usize> {
391 self.inner.last_start_offset()
392 }
393
394 #[inline]
398 pub fn src_bytes(&self) -> &'src [u8] {
399 self.inner.src_bytes()
400 }
401
402 #[inline]
407 pub fn line_col(&self) -> (u32, u32) {
408 crate::scanner::compute_line_col(self.src_bytes(), self.src_offset())
409 }
410
411 #[inline]
416 pub fn line_col_at(&self, offset: usize) -> (u32, u32) {
417 crate::scanner::compute_line_col(self.src_bytes(), offset)
418 }
419
420 #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Result<Event<'_, 'src>> {
430 match self.inner.next()? {
431 BytesEvent::StartElement(t) => Ok(Event::StartElement(StartTag { inner: t })),
432 BytesEvent::EndElement(t) => Ok(Event::EndElement(EndTag { inner: t })),
433 BytesEvent::Text(t) => Ok(Event::Text(Text { inner: t })),
434 BytesEvent::CData(s) => Ok(Event::CData(CData { inner: s })),
435 BytesEvent::Comment(s) => Ok(Event::Comment(Comment { inner: s })),
436 BytesEvent::Pi(p) => Ok(Event::Pi(Pi { inner: p })),
437 BytesEvent::EntityRef(e) => Ok(Event::EntityRef(EntityRef { inner: e })),
438 BytesEvent::Eof => Ok(Event::Eof),
439 }
440 }
441
442 pub fn next_into(&mut self, buf: &mut Vec<Attr<'src>>) -> Result<EventInto<'src>> {
452 buf.clear();
453 match self.next()? {
454 Event::StartElement(tag) => {
455 let name = tag.name_cow();
456 for attr in tag.attrs() { buf.push(attr?); }
457 Ok(EventInto::StartElement { name })
458 }
459 Event::EndElement(tag) => Ok(EventInto::EndElement {
460 name: Cow::Borrowed(tag.name()),
461 }),
462 Event::Text(t) => Ok(EventInto::Text(t.into_str())),
463 Event::CData(s) => Ok(EventInto::CData(s.into_str())),
464 Event::Comment(s) => Ok(EventInto::Comment(s.into_str())),
465 Event::Pi(p) => {
466 let (target, content) = p.into_parts();
467 Ok(EventInto::Pi { target, content })
468 }
469 Event::EntityRef(e) => Ok(EventInto::EntityRef {
470 name: Cow::Borrowed(e.name()),
471 }),
472 Event::Eof => Ok(EventInto::Eof),
473 }
474 }
475}
476
477#[inline]
485fn cow_bytes_to_str(c: Cow<'_, [u8]>) -> Cow<'_, str> {
486 match c {
487 Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
492 Cow::Owned(v) => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
493 }
494}
495
496pub fn unescape(s: &str) -> Cow<'_, str> {
510 if memchr(b'&', s.as_bytes()).is_none() {
511 return Cow::Borrowed(s);
512 }
513 let bytes = s.as_bytes();
514 let mut out = String::with_capacity(s.len());
515 let mut i = 0;
516 while i < bytes.len() {
517 if bytes[i] != b'&' {
518 out.push(bytes[i] as char);
519 i += 1;
520 continue;
521 }
522 let rest = &bytes[i + 1..];
524 let semi = match memchr(b';', rest) {
525 Some(n) if n <= 16 => n,
526 _ => { out.push('&'); i += 1; continue; }
527 };
528 let name = &rest[..semi];
529 match name {
530 b"amp" => { out.push('&'); }
531 b"lt" => { out.push('<'); }
532 b"gt" => { out.push('>'); }
533 b"quot" => { out.push('"'); }
534 b"apos" => { out.push('\''); }
535 _ if name.starts_with(b"#") => {
536 let cp: Option<u32> = if name.len() >= 2 && (name[1] == b'x' || name[1] == b'X') {
537 std::str::from_utf8(&name[2..]).ok()
538 .and_then(|h| u32::from_str_radix(h, 16).ok())
539 } else {
540 std::str::from_utf8(&name[1..]).ok()
541 .and_then(|d| d.parse::<u32>().ok())
542 };
543 match cp.and_then(char::from_u32) {
544 Some(c) => out.push(c),
545 None => {
546 out.push('&');
547 out.push_str(unsafe { std::str::from_utf8_unchecked(name) });
548 out.push(';');
549 }
550 }
551 }
552 _ => {
553 out.push('&');
554 out.push_str(unsafe { std::str::from_utf8_unchecked(name) });
555 out.push(';');
556 }
557 }
558 i += 1 + semi + 1;
559 }
560 Cow::Owned(out)
561}
562
563#[cfg(test)]
566mod tests {
567 use super::*;
568
569 fn events(src: &str) -> Vec<String> {
570 let mut r = XmlReader::from_str(src);
571 let mut out = Vec::new();
572 let mut buf = Vec::new();
573 loop {
574 match r.next_into(&mut buf).unwrap() {
575 EventInto::StartElement { name } => {
576 let a: Vec<_> = buf.iter().map(|a| format!("{}={}", a.name, a.value)).collect();
577 if a.is_empty() { out.push(format!("<{name}>")); }
578 else { out.push(format!("<{name} {}>", a.join(" "))); }
579 }
580 EventInto::EndElement { name } => out.push(format!("</{name}>")),
581 EventInto::Text(t) => out.push(format!("T:{t}")),
582 EventInto::CData(s) => out.push(format!("CD:{s}")),
583 EventInto::Comment(s) => out.push(format!("C:{s}")),
584 EventInto::Pi { target, .. } => out.push(format!("PI:{target}")),
585 EventInto::EntityRef { name } => out.push(format!("E:{name}")),
586 EventInto::Eof => break,
587 }
588 }
589 out
590 }
591
592 #[test]
593 fn minimal() {
594 assert_eq!(events("<r/>"), vec!["<r>", "</r>"]);
595 }
596
597 #[test]
598 fn nested() {
599 assert_eq!(
600 events("<a><b>hello</b></a>"),
601 vec!["<a>", "<b>", "T:hello", "</b>", "</a>"],
602 );
603 }
604
605 #[test]
606 fn attributes_borrowed() {
607 let src = r#"<el id="1" class="x"/>"#;
608 let mut r = XmlReader::from_str(src);
609 let mut buf = Vec::new();
610 let ev = r.next_into(&mut buf).unwrap();
611 assert!(matches!(&ev, EventInto::StartElement { name } if name == "el"));
612 assert_eq!(buf.len(), 2);
613 assert!(matches!(buf[0].value, Cow::Borrowed(_)), "no entity → should borrow");
614 }
615
616 #[test]
617 fn attribute_entity_owned() {
618 let src = r#"<el v="a&b"/>"#;
619 let mut r = XmlReader::from_str(src);
620 let mut buf = Vec::new();
621 r.next_into(&mut buf).unwrap();
622 assert_eq!(buf[0].value.as_ref(), "a&b");
623 assert!(matches!(buf[0].value, Cow::Owned(_)), "entity → must allocate");
624 }
625
626 #[test]
627 fn text_borrowed() {
628 let src = "<r>hello world</r>";
629 let mut r = XmlReader::from_str(src);
630 let mut buf = Vec::new();
631 r.next_into(&mut buf).unwrap(); let ev = r.next_into(&mut buf).unwrap();
633 assert!(matches!(ev, EventInto::Text(Cow::Borrowed("hello world"))));
634 }
635
636 #[test]
637 fn cdata_borrowed() {
638 let src = "<r><![CDATA[raw <data>]]></r>";
639 assert_eq!(events(src), vec!["<r>", "CD:raw <data>", "</r>"]);
640 }
641
642 #[test]
643 fn empty_element_emits_both_events() {
644 assert_eq!(events("<root><br/></root>"), vec!["<root>", "<br>", "</br>", "</root>"]);
645 }
646
647 #[test]
648 fn buffer_reuse() {
649 let src = "<a x='1'/><b y='2'/>";
650 let src = format!("<root>{src}</root>");
651 let mut r = XmlReader::from_str(&src);
652 let mut buf: Vec<Attr> = Vec::new();
653 let cap_before;
654 loop {
655 match r.next_into(&mut buf).unwrap() {
656 EventInto::StartElement { name } if name == "a" => {
657 cap_before = buf.capacity();
658 break;
659 }
660 EventInto::Eof => panic!("unexpected EOF"),
661 _ => {}
662 }
663 }
664 loop {
665 match r.next_into(&mut buf).unwrap() {
666 EventInto::StartElement { name } if name == "b" => {
667 assert_eq!(buf.capacity(), cap_before, "capacity should not grow for same-size attrs");
668 break;
669 }
670 EventInto::Eof => panic!("unexpected EOF"),
671 _ => {}
672 }
673 }
674 }
675
676 #[test]
677 fn lazy_attrs_iter() {
678 let src = r#"<el id="1" class="x"/>"#;
679 let mut r = XmlReader::from_str(src);
680 match r.next().unwrap() {
681 Event::StartElement(tag) => {
682 assert_eq!(tag.name(), "el");
683 let pairs: Vec<(String, String)> = tag.attrs()
684 .map(|a| a.map(|a| (a.name.to_owned(), a.value.into_owned())).unwrap())
685 .collect();
686 assert_eq!(pairs, vec![("id".into(), "1".into()), ("class".into(), "x".into())]);
687 }
688 _ => panic!("expected StartElement"),
689 }
690 }
691
692 #[test]
693 fn lazy_attrs_skipped_costs_nothing() {
694 let src = r#"<el id="1" class="x"/>"#;
695 let mut r = XmlReader::from_str(src);
696 match r.next().unwrap() {
697 Event::StartElement(tag) => assert_eq!(tag.name(), "el"),
698 _ => panic!(),
699 }
700 match r.next().unwrap() {
701 Event::EndElement(tag) => assert_eq!(tag.name(), "el"),
702 _ => panic!(),
703 }
704 match r.next().unwrap() {
705 Event::Eof => {}
706 _ => panic!(),
707 }
708 }
709
710 #[test]
711 fn debug_impl_shows_contents() {
712 let mut r = XmlReader::from_str("<a x='1'>hi<!-- c --><![CDATA[cd]]><?p y?></a>");
715 let mut seen: Vec<String> = Vec::new();
716 loop {
717 match r.next().unwrap() {
718 Event::Eof => break,
719 ev => seen.push(format!("{ev:?}")),
720 }
721 }
722 let combined = seen.join("\n");
723 assert!(combined.contains("StartTag"), "Debug missing StartTag — {combined}");
724 assert!(combined.contains("\"a\""), "Debug should show element name — {combined}");
725 assert!(combined.contains("Comment(\" c \")"), "Debug for Comment — {combined}");
726 assert!(combined.contains("CData(\"cd\")"), "Debug for CData — {combined}");
727 assert!(combined.contains("EndTag"), "Debug missing EndTag — {combined}");
728 }
729
730 #[test]
731 fn text_method_access() {
732 let src = "<r>hello</r>";
734 let mut r = XmlReader::from_str(src);
735 let _ = r.next().unwrap(); match r.next().unwrap() {
737 Event::Text(t) => {
738 assert_eq!(t.as_str(), "hello");
739 let owned = t.into_str();
740 assert_eq!(owned, "hello");
741 assert!(matches!(owned, Cow::Borrowed("hello")));
742 }
743 _ => panic!(),
744 }
745 }
746
747 #[test]
750 fn attr_name_and_value_accessors() {
751 let src = r#"<el id="1" class="x"/>"#;
752 let mut r = XmlReader::from_str(src);
753 match r.next().unwrap() {
754 Event::StartElement(tag) => {
755 let attrs: Vec<_> = tag.attrs().map(|a| a.unwrap()).collect();
756 assert_eq!(attrs[0].name(), "id");
758 assert_eq!(attrs[0].value(), "1");
759 assert_eq!(attrs[1].name(), "class");
760 assert_eq!(attrs[1].value(), "x");
761 }
762 _ => panic!(),
763 }
764 }
765
766 #[test]
769 fn from_bytes_valid_utf8() {
770 let src = b"<r/>";
771 let mut r = XmlReader::from_bytes(src).expect("valid utf-8");
772 match r.next().unwrap() {
773 Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
774 _ => panic!(),
775 }
776 }
777
778 #[test]
779 fn from_bytes_invalid_utf8_errors() {
780 let bad = &[b'<', 0xFF, 0xFE, b'>'];
781 assert!(XmlReader::from_bytes(bad).is_err());
782 }
783
784 #[test]
785 fn from_bytes_unchecked_skips_validation() {
786 let src = b"<r/>";
787 let mut r = unsafe { XmlReader::from_bytes_unchecked(src) };
789 match r.next().unwrap() {
790 Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
791 _ => panic!(),
792 }
793 }
794
795 #[test]
796 fn from_bytes_in_place_unchecked() {
797 let mut src = b"<r/>".to_vec();
798 let mut r = unsafe { XmlReader::from_bytes_in_place_unchecked(&mut src) };
801 match r.next().unwrap() {
802 Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
803 _ => panic!(),
804 }
805 drop(r);
806 }
807
808 #[test]
811 fn with_options_changes_behavior() {
812 let opts = ParseOptions { skip_entity_expansion: true, ..ParseOptions::default() };
815 let mut r = XmlReader::from_str("<r>&</r>").with_options(opts);
816 let _ = r.next().unwrap(); match r.next().unwrap() {
818 Event::Text(t) => assert_eq!(t.as_str(), "&"),
819 ev => panic!("expected Text, got {ev:?}"),
820 }
821 }
822
823 #[test]
824 fn xml_decl_returns_some_after_reading_prolog() {
825 let mut r = XmlReader::from_str(r#"<?xml version="1.0" encoding="UTF-8"?><r/>"#);
826 let _ = r.next().unwrap(); let decl = r.xml_decl().expect("xml decl");
828 assert_eq!(decl.version, "1.0");
829 }
830
831 #[test]
832 fn xml_decl_returns_none_when_absent() {
833 let mut r = XmlReader::from_str("<r/>");
834 let _ = r.next().unwrap();
835 assert!(r.xml_decl().is_none());
836 }
837
838 #[test]
839 fn recovered_errors_empty_for_well_formed_input() {
840 let mut r = XmlReader::from_str("<r/>");
841 let _ = r.next().unwrap();
842 let _ = r.next().unwrap();
843 assert!(r.recovered_errors().is_empty());
844 }
845
846 #[test]
849 fn entity_ref_event_via_next() {
850 let src = r#"<?xml version="1.0"?>
855<!DOCTYPE r [<!ENTITY foo "bar">]>
856<r>&foo;</r>"#;
857 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
858 let mut r = XmlReader::from_str(src).with_options(opts);
859 let mut found = false;
860 loop {
861 match r.next().unwrap() {
862 Event::EntityRef(e) => {
863 assert_eq!(e.name(), "foo");
864 let s = format!("{e:?}");
865 assert!(s.contains("foo"), "got {s}");
866 found = true;
867 break;
868 }
869 Event::Eof => break,
870 _ => continue,
871 }
872 }
873 assert!(found, "EntityRef event was not emitted");
874 }
875
876 #[test]
877 fn entity_ref_event_via_next_into() {
878 let src = r#"<?xml version="1.0"?>
879<!DOCTYPE r [<!ENTITY bar "x">]>
880<r>&bar;</r>"#;
881 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
882 let mut r = XmlReader::from_str(src).with_options(opts);
883 let mut buf = Vec::new();
884 let mut found = false;
885 loop {
886 match r.next_into(&mut buf).unwrap() {
887 EventInto::EntityRef { name } => {
888 assert_eq!(name, "bar");
889 found = true;
890 break;
891 }
892 EventInto::Eof => break,
893 _ => continue,
894 }
895 }
896 assert!(found, "EntityRef event was not emitted");
897 }
898
899 #[test]
902 fn unescape_no_amp_returns_borrowed() {
903 let s = "no entities here";
904 let out = unescape(s);
905 assert!(matches!(out, Cow::Borrowed(_)));
906 assert_eq!(out, "no entities here");
907 }
908
909 #[test]
910 fn unescape_predefined_entities() {
911 assert_eq!(unescape("&").as_ref(), "&");
912 assert_eq!(unescape("<").as_ref(), "<");
913 assert_eq!(unescape(">").as_ref(), ">");
914 assert_eq!(unescape(""").as_ref(), "\"");
915 assert_eq!(unescape("'").as_ref(), "'");
916 }
917
918 #[test]
919 fn unescape_mixed_text() {
920 assert_eq!(
921 unescape("a & b < c > d").as_ref(),
922 "a & b < c > d",
923 );
924 }
925
926 #[test]
927 fn unescape_numeric_decimal_char_ref() {
928 assert_eq!(unescape("A").as_ref(), "A");
929 assert_eq!(unescape("€").as_ref(), "€");
930 }
931
932 #[test]
933 fn unescape_numeric_hex_char_ref() {
934 assert_eq!(unescape("A").as_ref(), "A");
935 assert_eq!(unescape("&#xX41;").as_ref(), "&#xX41;"); assert_eq!(unescape("A").as_ref(), "A");
937 }
938
939 #[test]
940 fn unescape_invalid_char_ref_passes_through() {
941 assert_eq!(unescape("&#abc;").as_ref(), "&#abc;");
943 assert_eq!(unescape("�").as_ref(), "�");
945 }
946
947 #[test]
948 fn unescape_unknown_named_entity_passes_through() {
949 assert_eq!(unescape("&bogus;").as_ref(), "&bogus;");
951 }
952
953 #[test]
954 fn unescape_ampersand_without_semicolon() {
955 assert_eq!(unescape("&just an ampersand").as_ref(), "&just an ampersand");
957 assert_eq!(unescape("&very_long_name_that_exceeds_sixteen_chars_threshold;").as_ref(),
959 "&very_long_name_that_exceeds_sixteen_chars_threshold;");
960 }
961}