1pub fn escape_xml(s: &str) -> String {
17 let mut result = String::with_capacity(s.len());
18 for ch in s.chars() {
19 match ch {
20 '&' => result.push_str("&"),
21 '<' => result.push_str("<"),
22 '>' => result.push_str(">"),
23 '"' => result.push_str("""),
24 '\'' => result.push_str("'"),
25 c => result.push(c),
26 }
27 }
28 result
29}
30
31pub fn extract_xml_tag_value(xml: &str, tag_name: &str) -> Option<String> {
48 let open = format!("<{tag_name}>");
49 let close = format!("</{tag_name}>");
50 let start = xml.find(&open)? + open.len();
51 let end = xml[start..].find(&close)? + start;
52 Some(xml[start..end].to_string())
53}
54
55pub fn tag(name: &str, attrs: &[(&str, &str)], children: TagContent<'_>) -> String {
60 use std::fmt::Write as _;
61 let attr_str: String = attrs.iter().fold(String::new(), |mut s, (k, v)| {
62 let _ = write!(s, " {k}=\"{}\"", escape_xml(v));
63 s
64 });
65
66 match children {
67 TagContent::None => format!("<{name}{attr_str}></{name}>"),
68 TagContent::Text(text) => {
69 format!("<{name}{attr_str}>{}</{name}>", escape_xml(text))
70 }
71 TagContent::Children(kids) => {
72 let inner: String = kids.into_iter().collect();
73 format!("<{name}{attr_str}>{inner}</{name}>")
74 }
75 }
76}
77
78#[non_exhaustive]
84pub enum TagContent<'a> {
85 None,
87 Text(&'a str),
89 Children(Vec<String>),
91}
92
93impl<'a> From<&'a str> for TagContent<'a> {
94 fn from(s: &'a str) -> Self {
95 TagContent::Text(s)
96 }
97}
98
99impl From<Vec<String>> for TagContent<'_> {
100 fn from(v: Vec<String>) -> Self {
101 TagContent::Children(v)
102 }
103}
104
105impl From<String> for TagContent<'_> {
106 fn from(s: String) -> Self {
107 TagContent::Text(Box::leak(s.into_boxed_str()))
108 }
109}
110
111pub fn pretty_print_xml(xml: &str) -> String {
127 let mut tokens: Vec<XmlToken> = Vec::new();
129 let mut pos = 0;
130 let bytes = xml.as_bytes();
131
132 while pos < bytes.len() {
133 if bytes[pos] == b'<' {
134 let end = xml[pos..]
136 .find('>')
137 .map(|i| pos + i + 1)
138 .unwrap_or(bytes.len());
139 tokens.push(XmlToken::Tag(xml[pos..end].to_string()));
140 pos = end;
141 } else {
142 let end = xml[pos..].find('<').map(|i| pos + i).unwrap_or(bytes.len());
144 let text = &xml[pos..end];
145 if !text.trim().is_empty() {
146 tokens.push(XmlToken::Text(text.trim().to_string()));
147 }
148 pos = end;
149 }
150 }
151
152 let indent = " ";
154 let mut result = String::with_capacity(xml.len() * 2);
155 let mut depth: usize = 0;
156
157 let mut i = 0;
158 while i < tokens.len() {
159 match &tokens[i] {
160 XmlToken::Tag(t) if t.starts_with("<?") => {
161 result.push_str(t);
163 result.push('\n');
164 }
165 XmlToken::Tag(t) if t.starts_with("</") => {
166 depth = depth.saturating_sub(1);
168 for _ in 0..depth {
169 result.push_str(indent);
170 }
171 result.push_str(t);
172 result.push('\n');
173 }
174 XmlToken::Tag(t) if t.ends_with("/>") => {
175 for _ in 0..depth {
177 result.push_str(indent);
178 }
179 result.push_str(t);
180 result.push('\n');
181 }
182 XmlToken::Tag(t) => {
183 if i + 2 < tokens.len() {
185 if let (XmlToken::Text(text), XmlToken::Tag(close)) =
186 (&tokens[i + 1], &tokens[i + 2])
187 {
188 if close.starts_with("</") {
189 for _ in 0..depth {
191 result.push_str(indent);
192 }
193 result.push_str(t);
194 result.push_str(text);
195 result.push_str(close);
196 result.push('\n');
197 i += 3;
198 continue;
199 }
200 }
201 }
202 for _ in 0..depth {
203 result.push_str(indent);
204 }
205 result.push_str(t);
206 result.push('\n');
207 depth += 1;
208 }
209 XmlToken::Text(t) => {
210 for _ in 0..depth {
212 result.push_str(indent);
213 }
214 result.push_str(t);
215 result.push('\n');
216 }
217 }
218 i += 1;
219 }
220
221 while result.ends_with('\n') {
223 result.pop();
224 }
225 result
226}
227
228enum XmlToken {
230 Tag(String),
231 Text(String),
232}
233
234pub fn replace_unacceptable_characters(input: &str) -> String {
269 if input.is_empty() {
270 return String::new();
271 }
272
273 let s = input.replace(['<', '>'], "");
275
276 let s = s.replace('&', " & ");
278
279 let s = s.replace(['\'', '"'], "");
281
282 let s = collapse_whitespace(&s);
284
285 let s = s.replace('&', "&");
287
288 let s = s.replace(['\r', '\t', '\n'], "");
290
291 let s = collapse_whitespace(&s);
293
294 let s: String = s
296 .chars()
297 .filter(|&c| !c.is_ascii_control() || c == ' ')
298 .collect();
299
300 s.trim().to_string()
302}
303
304fn collapse_whitespace(s: &str) -> String {
309 let mut result = String::with_capacity(s.len());
310 let mut prev_ws = false;
311 for ch in s.chars() {
312 if ch.is_whitespace() {
313 if !prev_ws {
314 result.push(' ');
315 }
316 prev_ws = true;
317 } else {
318 result.push(ch);
319 prev_ws = false;
320 }
321 }
322 result
323}
324
325pub fn validate_xml(xml: &str) -> Result<(), crate::FiscalError> {
354 let mut errors: Vec<String> = Vec::new();
355
356 let required_structure = [
358 ("NFe", "Elemento raiz <NFe> ausente"),
359 ("infNFe", "Elemento <infNFe> ausente"),
360 ];
361 for (tag_name, msg) in &required_structure {
362 if !xml.contains(&format!("<{tag_name}")) {
363 errors.push(msg.to_string());
364 }
365 }
366
367 let ide_tags = [
369 "cUF", "cNF", "natOp", "mod", "serie", "nNF", "dhEmi", "tpNF", "idDest", "cMunFG", "tpImp",
370 "tpEmis", "cDV", "tpAmb", "finNFe", "indFinal", "indPres", "procEmi", "verProc",
371 ];
372 for tag_name in &ide_tags {
373 if extract_xml_tag_value(xml, tag_name).is_none() {
374 errors.push(format!("Tag obrigatória <{tag_name}> ausente em <ide>"));
375 }
376 }
377
378 let emit_required = ["xNome", "IE", "CRT"];
380 for tag_name in &emit_required {
381 if extract_xml_tag_value(xml, tag_name).is_none() {
382 errors.push(format!("Tag obrigatória <{tag_name}> ausente em <emit>"));
383 }
384 }
385 if extract_xml_tag_value(xml, "CNPJ").is_none() && extract_xml_tag_value(xml, "CPF").is_none() {
387 errors.push("Tag <CNPJ> ou <CPF> ausente em <emit>".to_string());
388 }
389
390 let required_blocks = [
392 ("enderEmit", "Bloco <enderEmit> ausente"),
393 ("det ", "Nenhum item <det> encontrado"),
394 ("total", "Bloco <total> ausente"),
395 ("ICMSTot", "Bloco <ICMSTot> ausente"),
396 ("transp", "Bloco <transp> ausente"),
397 ("pag", "Bloco <pag> ausente"),
398 ];
399 for (fragment, msg) in &required_blocks {
400 if !xml.contains(&format!("<{fragment}")) {
401 errors.push(msg.to_string());
402 }
403 }
404
405 if let Some(id_start) = xml.find("Id=\"NFe") {
407 let after_id = &xml[id_start + 7..];
408 if let Some(quote_end) = after_id.find('"') {
409 let key = &after_id[..quote_end];
410 if key.len() != 44 || !key.chars().all(|c| c.is_ascii_digit()) {
411 errors.push(format!(
412 "Chave de acesso inválida: esperado 44 dígitos, encontrado '{key}'"
413 ));
414 }
415 }
416 }
417
418 if errors.is_empty() {
419 Ok(())
420 } else {
421 Err(crate::FiscalError::XmlParsing(errors.join("; ")))
422 }
423}
424
425pub fn remove_invalid_xml_chars(input: &str) -> String {
450 let mut result = String::with_capacity(input.len());
451 for ch in input.chars() {
452 if is_valid_xml_char(ch) {
453 result.push(ch);
454 }
455 }
456 result
457}
458
459fn is_valid_xml_char(ch: char) -> bool {
464 matches!(ch,
465 '\u{09}' | '\u{0A}' | '\u{0D}' |
466 '\u{20}'..='\u{D7FF}' |
467 '\u{E000}'..='\u{FFFD}' |
468 '\u{10000}'..='\u{10FFFF}'
469 )
470}
471
472pub fn clear_xml_string(input: &str, remove_encoding_tag: bool) -> String {
497 let mut result = input.to_string();
499
500 let removals = [
501 "xmlns:default=\"http://www.w3.org/2000/09/xmldsig#\"",
502 " standalone=\"no\"",
503 "default:",
504 ":default",
505 "\n",
506 "\r",
507 "\t",
508 ];
509 for pattern in &removals {
510 result = result.replace(pattern, "");
511 }
512
513 let mut collapsed = String::with_capacity(result.len());
516 let mut chars = result.chars().peekable();
517 while let Some(ch) = chars.next() {
518 collapsed.push(ch);
519 if ch == '>' {
520 let mut ws_buf = String::new();
522 while let Some(&next) = chars.peek() {
523 if next.is_ascii_whitespace() {
524 ws_buf.push(next);
525 chars.next();
526 } else {
527 break;
528 }
529 }
530 if let Some(&next) = chars.peek() {
533 if next != '<' {
534 collapsed.push_str(&ws_buf);
535 }
536 } else {
537 collapsed.push_str(&ws_buf);
539 }
540 }
541 }
542 result = collapsed;
543
544 if remove_encoding_tag {
546 result = delete_all_between(&result, "<?xml", "?>");
547 }
548
549 result
550}
551
552fn delete_all_between(input: &str, beginning: &str, end: &str) -> String {
557 let begin_pos = match input.find(beginning) {
558 Some(p) => p,
559 None => return input.to_string(),
560 };
561 let after_begin = begin_pos + beginning.len();
562 let end_pos = match input[after_begin..].find(end) {
563 Some(p) => after_begin + p + end.len(),
564 None => return input.to_string(),
565 };
566 let mut result = String::with_capacity(input.len() - (end_pos - begin_pos));
567 result.push_str(&input[..begin_pos]);
568 result.push_str(&input[end_pos..]);
569 result
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn pretty_print_simple_xml() {
578 let compact = "<root><child>text</child></root>";
579 let pretty = pretty_print_xml(compact);
580 assert!(pretty.contains("<root>"));
581 assert!(pretty.contains(" <child>text</child>"));
582 assert!(pretty.contains("</root>"));
583 }
584
585 #[test]
586 fn pretty_print_nested_xml() {
587 let compact = "<a><b><c>val</c></b></a>";
588 let pretty = pretty_print_xml(compact);
589 let lines: Vec<&str> = pretty.lines().collect();
590 assert_eq!(lines[0], "<a>");
591 assert_eq!(lines[1], " <b>");
592 assert_eq!(lines[2], " <c>val</c>");
593 assert_eq!(lines[3], " </b>");
594 assert_eq!(lines[4], "</a>");
595 }
596
597 #[test]
598 fn pretty_print_with_declaration() {
599 let xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><a>1</a></root>";
600 let pretty = pretty_print_xml(xml);
601 assert!(pretty.starts_with("<?xml"));
602 assert!(pretty.contains(" <a>1</a>"));
603 }
604
605 #[test]
606 fn pretty_print_empty_input() {
607 let pretty = pretty_print_xml("");
608 assert_eq!(pretty, "");
609 }
610
611 #[test]
612 fn validate_xml_valid_nfe() {
613 let xml = concat!(
614 r#"<NFe><infNFe versao="4.00" Id="NFe41260304123456000190550010000001231123456780">"#,
615 "<ide><cUF>41</cUF><cNF>12345678</cNF><natOp>VENDA</natOp>",
616 "<mod>55</mod><serie>1</serie><nNF>123</nNF>",
617 "<dhEmi>2026-03-11T10:30:00-03:00</dhEmi>",
618 "<tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG>",
619 "<tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>0</cDV>",
620 "<tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal>",
621 "<indPres>1</indPres><procEmi>0</procEmi><verProc>1.0</verProc></ide>",
622 "<emit><CNPJ>04123456000190</CNPJ><xNome>Test</xNome>",
623 "<enderEmit><xLgr>Rua</xLgr></enderEmit>",
624 "<IE>9012345678</IE><CRT>3</CRT></emit>",
625 "<det nItem=\"1\"><prod><cProd>001</cProd></prod></det>",
626 "<total><ICMSTot><vNF>150.00</vNF></ICMSTot></total>",
627 "<transp><modFrete>9</modFrete></transp>",
628 "<pag><detPag><tPag>01</tPag><vPag>150.00</vPag></detPag></pag>",
629 "</infNFe></NFe>",
630 );
631 assert!(validate_xml(xml).is_ok());
632 }
633
634 #[test]
635 fn validate_xml_missing_tags() {
636 let xml = "<root><something>val</something></root>";
637 let err = validate_xml(xml).unwrap_err();
638 let msg = err.to_string();
639 assert!(msg.contains("NFe"));
640 assert!(msg.contains("infNFe"));
641 }
642
643 #[test]
644 fn validate_xml_invalid_access_key() {
645 let xml = concat!(
646 r#"<NFe><infNFe versao="4.00" Id="NFe123">"#,
647 "<ide><cUF>41</cUF><cNF>12345678</cNF><natOp>VENDA</natOp>",
648 "<mod>55</mod><serie>1</serie><nNF>123</nNF>",
649 "<dhEmi>2026-03-11T10:30:00-03:00</dhEmi>",
650 "<tpNF>1</tpNF><idDest>1</idDest><cMunFG>4106902</cMunFG>",
651 "<tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>0</cDV>",
652 "<tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal>",
653 "<indPres>1</indPres><procEmi>0</procEmi><verProc>1.0</verProc></ide>",
654 "<emit><CNPJ>04123456000190</CNPJ><xNome>Test</xNome>",
655 "<enderEmit><xLgr>Rua</xLgr></enderEmit>",
656 "<IE>9012345678</IE><CRT>3</CRT></emit>",
657 "<det nItem=\"1\"><prod><cProd>001</cProd></prod></det>",
658 "<total><ICMSTot><vNF>150.00</vNF></ICMSTot></total>",
659 "<transp><modFrete>9</modFrete></transp>",
660 "<pag><detPag><tPag>01</tPag><vPag>150.00</vPag></detPag></pag>",
661 "</infNFe></NFe>",
662 );
663 let err = validate_xml(xml).unwrap_err();
664 let msg = err.to_string();
665 assert!(msg.contains("Chave de acesso"));
666 }
667
668 #[test]
671 fn remove_invalid_xml_chars_preserves_valid_text() {
672 assert_eq!(remove_invalid_xml_chars("Hello, World!"), "Hello, World!");
673 }
674
675 #[test]
676 fn remove_invalid_xml_chars_preserves_tab_lf_cr() {
677 assert_eq!(
679 remove_invalid_xml_chars("a\x09b\x0Ac\x0Dd"),
680 "a\x09b\x0Ac\x0Dd"
681 );
682 }
683
684 #[test]
685 fn remove_invalid_xml_chars_strips_null_and_low_controls() {
686 assert_eq!(
688 remove_invalid_xml_chars("\x00\x01\x02\x03\x04\x05\x06\x07\x08hello"),
689 "hello"
690 );
691 }
692
693 #[test]
694 fn remove_invalid_xml_chars_strips_0b_0c() {
695 assert_eq!(remove_invalid_xml_chars("a\x0Bb\x0Cc"), "abc");
697 }
698
699 #[test]
700 fn remove_invalid_xml_chars_strips_0e_to_1f() {
701 let mut input = String::from("ok");
703 for byte in 0x0Eu8..=0x1F {
704 input.push(byte as char);
705 }
706 input.push_str("end");
707 assert_eq!(remove_invalid_xml_chars(&input), "okend");
708 }
709
710 #[test]
711 fn remove_invalid_xml_chars_strips_del() {
712 assert_eq!(remove_invalid_xml_chars("a\x7Fb"), "a\x7Fb");
720 }
721
722 #[test]
723 fn remove_invalid_xml_chars_strips_fffe_ffff() {
724 let input = format!("a{}b{}c", '\u{FFFE}', '\u{FFFF}');
726 assert_eq!(remove_invalid_xml_chars(&input), "abc");
727 }
728
729 #[test]
730 fn remove_invalid_xml_chars_preserves_bmp_and_supplementary() {
731 assert_eq!(
733 remove_invalid_xml_chars("café résumé 日本語"),
734 "café résumé 日本語"
735 );
736 let input = "hello \u{1F600} world"; assert_eq!(remove_invalid_xml_chars(input), input);
739 }
740
741 #[test]
742 fn remove_invalid_xml_chars_preserves_private_use_area() {
743 let input = "a\u{E000}b\u{FFFD}c";
745 assert_eq!(remove_invalid_xml_chars(input), input);
746 }
747
748 #[test]
749 fn remove_invalid_xml_chars_empty_string() {
750 assert_eq!(remove_invalid_xml_chars(""), "");
751 }
752
753 #[test]
754 fn remove_invalid_xml_chars_all_invalid() {
755 assert_eq!(remove_invalid_xml_chars("\x00\x01\x02\x03"), "");
756 }
757
758 #[test]
759 fn remove_invalid_xml_chars_mixed_xml_content() {
760 let input = "<tag>val\x00ue with \x0Bcontrol\x1F chars</tag>";
761 assert_eq!(
762 remove_invalid_xml_chars(input),
763 "<tag>value with control chars</tag>"
764 );
765 }
766
767 #[test]
770 fn clear_xml_string_removes_whitespace_between_tags() {
771 let xml = "<root>\n <child>text</child>\n</root>";
772 assert_eq!(
773 clear_xml_string(xml, false),
774 "<root><child>text</child></root>"
775 );
776 }
777
778 #[test]
779 fn clear_xml_string_removes_tabs_cr_lf() {
780 let xml = "<a>\t<b>\r\n<c>val</c>\n</b>\n</a>";
781 assert_eq!(clear_xml_string(xml, false), "<a><b><c>val</c></b></a>");
782 }
783
784 #[test]
785 fn clear_xml_string_removes_default_namespace() {
786 let xml = "<Signature xmlns:default=\"http://www.w3.org/2000/09/xmldsig#\"><default:SignedInfo>data</default:SignedInfo></Signature>";
789 assert_eq!(
790 clear_xml_string(xml, false),
791 "<Signature ><SignedInfo>data</SignedInfo></Signature>"
792 );
793 }
794
795 #[test]
796 fn clear_xml_string_removes_standalone_no() {
797 let xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><root/>";
798 assert_eq!(
799 clear_xml_string(xml, false),
800 "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
801 );
802 }
803
804 #[test]
805 fn clear_xml_string_removes_encoding_tag() {
806 let xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><a>1</a></root>";
807 assert_eq!(clear_xml_string(xml, true), "<root><a>1</a></root>");
808 }
809
810 #[test]
811 fn clear_xml_string_preserves_without_encoding_tag() {
812 let xml = "<?xml version=\"1.0\"?><root><a>1</a></root>";
813 assert_eq!(
814 clear_xml_string(xml, false),
815 "<?xml version=\"1.0\"?><root><a>1</a></root>"
816 );
817 }
818
819 #[test]
820 fn clear_xml_string_no_encoding_tag_present() {
821 let xml = "<root><a>1</a></root>";
822 assert_eq!(clear_xml_string(xml, true), "<root><a>1</a></root>");
823 }
824
825 #[test]
826 fn clear_xml_string_empty_input() {
827 assert_eq!(clear_xml_string("", false), "");
828 assert_eq!(clear_xml_string("", true), "");
829 }
830
831 #[test]
832 fn clear_xml_string_preserves_text_content_spaces() {
833 let xml = "<tag>hello world</tag>";
835 assert_eq!(clear_xml_string(xml, false), "<tag>hello world</tag>");
836 }
837
838 #[test]
839 fn clear_xml_string_collapses_multiple_spaces_between_tags() {
840 let xml = "<a> <b>text</b> </a>";
841 assert_eq!(clear_xml_string(xml, false), "<a><b>text</b></a>");
842 }
843
844 #[test]
845 fn clear_xml_string_removes_colon_default_suffix() {
846 let xml = "<Signature:default><data/></Signature:default>";
847 assert_eq!(
848 clear_xml_string(xml, false),
849 "<Signature><data/></Signature>"
850 );
851 }
852
853 #[test]
856 fn replace_unacceptable_empty() {
857 assert_eq!(replace_unacceptable_characters(""), "");
858 }
859
860 #[test]
861 fn replace_unacceptable_plain_text() {
862 assert_eq!(
863 replace_unacceptable_characters("Venda de mercadorias"),
864 "Venda de mercadorias"
865 );
866 }
867
868 #[test]
869 fn replace_unacceptable_removes_angle_brackets() {
870 assert_eq!(replace_unacceptable_characters("foo<bar>baz"), "foobarbaz");
871 }
872
873 #[test]
874 fn replace_unacceptable_ampersand_encoding() {
875 assert_eq!(replace_unacceptable_characters("A&B"), "A & B");
876 }
877
878 #[test]
879 fn replace_unacceptable_removes_quotes() {
880 assert_eq!(
881 replace_unacceptable_characters(r#"It's a "test""#),
882 "Its a test"
883 );
884 }
885
886 #[test]
887 fn replace_unacceptable_collapses_whitespace() {
888 assert_eq!(
889 replace_unacceptable_characters("hello world"),
890 "hello world"
891 );
892 }
893
894 #[test]
895 fn replace_unacceptable_trims() {
896 assert_eq!(replace_unacceptable_characters(" hello "), "hello");
897 }
898
899 #[test]
900 fn replace_unacceptable_removes_control_chars() {
901 assert_eq!(
902 replace_unacceptable_characters("abc\x00\x01\x02def"),
903 "abcdef"
904 );
905 }
906
907 #[test]
908 fn replace_unacceptable_removes_cr_lf_tab() {
909 assert_eq!(
910 replace_unacceptable_characters("line1\r\n\tline2"),
911 "line1 line2"
912 );
913 }
914
915 #[test]
916 fn replace_unacceptable_combined() {
917 assert_eq!(
918 replace_unacceptable_characters(
919 " Cancelamento <por> erro & \"duplicidade\" na emissão\t\n "
920 ),
921 "Cancelamento por erro & duplicidade na emissão"
922 );
923 }
924
925 #[test]
926 fn replace_unacceptable_ampersand_already_spaced() {
927 assert_eq!(replace_unacceptable_characters("A & B"), "A & B");
928 }
929
930 #[test]
931 fn replace_unacceptable_multiple_ampersands() {
932 assert_eq!(
933 replace_unacceptable_characters("A&B&C"),
934 "A & B & C"
935 );
936 }
937
938 #[test]
939 fn replace_unacceptable_preserves_accented_chars() {
940 assert_eq!(
941 replace_unacceptable_characters("São Paulo — café"),
942 "São Paulo — café"
943 );
944 }
945
946 #[test]
947 fn replace_unacceptable_only_special_chars() {
948 assert_eq!(replace_unacceptable_characters("<>\"'"), "");
949 }
950
951 #[test]
952 fn replace_unacceptable_del_char() {
953 assert_eq!(replace_unacceptable_characters("abc\x7Fdef"), "abcdef");
954 }
955
956 #[test]
959 fn tag_content_from_string() {
960 let content: TagContent = String::from("hello").into();
961 match content {
962 TagContent::Text(t) => assert_eq!(t, "hello"),
963 _ => panic!("expected Text"),
964 }
965 }
966
967 #[test]
968 fn tag_content_from_vec_string() {
969 let content: TagContent = vec!["<a/>".to_string(), "<b/>".to_string()].into();
970 match content {
971 TagContent::Children(kids) => assert_eq!(kids.len(), 2),
972 _ => panic!("expected Children"),
973 }
974 }
975
976 #[test]
979 fn pretty_print_self_closing_tag() {
980 let xml = "<root><empty/></root>";
981 let pretty = pretty_print_xml(xml);
982 assert!(pretty.contains(" <empty/>"));
983 }
984
985 #[test]
986 fn pretty_print_standalone_text() {
987 let xml = "<root><a><b>text</b></a></root>";
989 let pretty = pretty_print_xml(xml);
990 assert!(pretty.contains(" <b>text</b>"));
991 }
992
993 #[test]
996 fn clear_xml_string_non_tag_after_whitespace() {
997 let xml = "<a>text after close</a>";
999 let result = clear_xml_string(xml, false);
1000 assert_eq!(result, "<a>text after close</a>");
1001 }
1002
1003 #[test]
1006 fn delete_all_between_no_match() {
1007 let result = delete_all_between("hello world", "<?xml", "?>");
1008 assert_eq!(result, "hello world");
1009 }
1010
1011 #[test]
1012 fn delete_all_between_no_end_match() {
1013 let result = delete_all_between("<?xml version start", "<?xml", "?>");
1014 assert_eq!(result, "<?xml version start");
1015 }
1016}