1use std::fmt::Write;
12
13use crate::ast::{Document, Element, Node};
14use crate::error::VexyError;
15
16#[derive(Debug, Clone)]
18pub struct StringifyConfig {
19 pub pretty: bool,
21 pub indent: String,
23 pub newlines: bool,
25 pub quote_attrs: bool,
27 pub self_close: bool,
29 pub initial_capacity: usize,
31}
32
33impl Default for StringifyConfig {
34 fn default() -> Self {
35 Self {
36 pretty: false,
37 indent: " ".to_string(),
38 newlines: false,
39 quote_attrs: true,
40 self_close: true,
41 initial_capacity: 4096, }
43 }
44}
45
46impl StringifyConfig {
47 pub fn minified() -> Self {
49 Self::default()
50 }
51
52 pub fn pretty() -> Self {
54 Self {
55 pretty: true,
56 newlines: true,
57 ..Default::default()
58 }
59 }
60}
61
62pub fn stringify(document: &Document) -> Result<String, VexyError> {
64 stringify_with_config(document, &StringifyConfig::default())
65}
66
67pub fn stringify_with_config(
69 document: &Document,
70 config: &StringifyConfig,
71) -> Result<String, VexyError> {
72 let estimated_size = estimate_document_size(document);
73 let mut output = String::with_capacity(estimated_size.max(config.initial_capacity));
74 stringify_into_buffer(document, config, &mut output)?;
75 Ok(output)
76}
77
78pub fn stringify_into_buffer(
83 document: &Document,
84 config: &StringifyConfig,
85 output: &mut String,
86) -> Result<(), VexyError> {
87 output.clear();
88
89 let estimated_size = estimate_document_size(document);
91 let needed = estimated_size.max(config.initial_capacity);
92 if output.capacity() < needed {
93 output.reserve(needed - output.len());
94 }
95
96 if let Some(ref version) = document.metadata.version {
98 write!(output, "<?xml version=\"{version}\"")?;
99 if let Some(ref encoding) = document.metadata.encoding {
100 write!(output, " encoding=\"{encoding}\"")?;
101 }
102 if let Some(ref standalone) = document.metadata.standalone {
103 write!(output, " standalone=\"{standalone}\"")?;
104 }
105 output.push_str("?>");
106 if config.newlines {
107 output.push('\n');
108 }
109 }
110
111 for node in &document.prologue {
113 stringify_node(node, &mut *output, config, 0)?;
114 if config.newlines && !matches!(node, Node::Text(_)) {
115 output.push('\n');
116 }
117 }
118
119 stringify_element(&document.root, &mut *output, config, 0)?;
121
122 if !document.epilogue.is_empty() && config.newlines {
124 output.push('\n');
125 }
126 for node in &document.epilogue {
127 stringify_node(node, &mut *output, config, 0)?;
128 if config.newlines && !matches!(node, Node::Text(_)) {
129 output.push('\n');
130 }
131 }
132
133 Ok(())
134}
135
136fn estimate_document_size(document: &Document) -> usize {
138 estimate_element_size(&document.root)
140 + document
141 .prologue
142 .iter()
143 .map(estimate_node_size)
144 .sum::<usize>()
145 + document
146 .epilogue
147 .iter()
148 .map(estimate_node_size)
149 .sum::<usize>()
150}
151
152fn estimate_element_size(element: &Element) -> usize {
153 let mut size = element.name.len() * 2 + 5; for (name, value) in &element.attributes {
157 size += name.len() + value.len() + 4; }
159
160 for (prefix, uri) in &element.namespaces {
162 if prefix.is_empty() {
163 size += 7 + uri.len(); } else {
165 size += 7 + prefix.len() + uri.len(); }
167 }
168
169 size += element
171 .children
172 .iter()
173 .map(estimate_node_size)
174 .sum::<usize>();
175
176 size
177}
178
179fn estimate_node_size(node: &Node) -> usize {
180 match node {
181 Node::Element(e) => estimate_element_size(e),
182 Node::Text(t) => t.len(),
183 Node::Comment(c) => c.len() + 7, Node::CData(c) => c.len() + 12, Node::ProcessingInstruction { target, data } => target.len() + data.len() + 5, Node::DocType(d) => d.len() + 11, }
188}
189
190fn stringify_element(
192 element: &Element,
193 output: &mut String,
194 config: &StringifyConfig,
195 depth: usize,
196) -> Result<(), VexyError> {
197 if config.pretty && depth > 0 {
199 let line_start = output.rfind('\n').map_or(0, |index| index + 1);
200 let line_prefix = &output[line_start..];
201 let has_existing_indent = !line_prefix.is_empty() && line_prefix.chars().all(|c| c == ' ');
202
203 if !has_existing_indent {
204 for _ in 0..depth {
205 output.push_str(&config.indent);
206 }
207 }
208 }
209
210 output.push('<');
212 output.push_str(&element.name);
213
214 for (name, value) in &element.attributes {
216 output.push(' ');
217 output.push_str(name);
218 if value.is_empty() {
219 output.push('=');
220 if config.quote_attrs {
221 output.push_str("\"\"");
222 }
223 continue;
224 }
225 output.push('=');
226 if value.starts_with('{') && value.ends_with('}') {
227 output.push_str(value);
228 } else if config.quote_attrs {
229 output.push('"');
230 output.push_str(value);
231 output.push('"');
232 } else {
233 output.push_str(value);
234 }
235 }
236
237 for (prefix, uri) in &element.namespaces {
239 let xmlns_key = if prefix.is_empty() {
240 "xmlns".to_string()
241 } else {
242 format!("xmlns:{prefix}")
243 };
244
245 if element.attributes.contains_key(xmlns_key.as_str()) {
246 continue;
247 }
248
249 output.push(' ');
250 output.push_str(&xmlns_key);
251 output.push('=');
252 if config.quote_attrs {
253 output.push('"');
254 output.push_str(uri);
255 output.push('"');
256 } else {
257 output.push_str(uri);
258 }
259 }
260
261 if element.children.is_empty() && config.self_close {
262 output.push_str("/>");
264 } else {
265 output.push('>');
267
268 let has_element_children = element
269 .children
270 .iter()
271 .any(|n| matches!(n, Node::Element(_)));
272 let has_non_empty_text = element.children.iter().any(|n| match n {
273 Node::Text(t) => !t.trim().is_empty(),
274 _ => false,
275 });
276 let starts_with_formatting_text = matches!(
277 element.children.first(),
278 Some(Node::Text(text)) if text.trim().is_empty() && text.contains('\n')
279 );
280 let ends_with_formatting_text = matches!(
281 element.children.last(),
282 Some(Node::Text(text)) if text.trim().is_empty() && text.contains('\n')
283 );
284 let inline_text_element = element.name == "text"
285 && !has_element_children
286 && element.children.len() == 1
287 && matches!(
288 element.children.first(),
289 Some(Node::Text(t)) if !t.trim().is_empty() && !t.contains('\n') && t.as_ref() == t.trim()
290 );
291 let has_content_children = has_element_children || has_non_empty_text;
292 let is_formatting_text =
293 |node: &Node| matches!(node, Node::Text(text) if text.trim().is_empty());
294
295 if has_content_children
296 && config.newlines
297 && !inline_text_element
298 && !starts_with_formatting_text
299 {
300 output.push('\n');
301 }
302
303 for (i, child) in element.children.iter().enumerate() {
305 match child {
306 Node::Element(_) => {
307 stringify_node(child, output, config, depth + 1)?;
308 let next_is_formatting = matches!(
309 element.children.get(i + 1),
310 Some(next_child) if is_formatting_text(next_child)
311 );
312 if config.newlines && i < element.children.len() - 1 && !next_is_formatting {
313 output.push('\n');
314 }
315 }
316 Node::Text(t) if !t.trim().is_empty() => {
317 let text_depth = if inline_text_element {
318 0
319 } else if element.name == "textPath" {
320 depth
321 } else {
322 depth + 1
323 };
324 stringify_node(child, output, config, text_depth)?;
325 let next_is_formatting = matches!(
326 element.children.get(i + 1),
327 Some(next_child) if is_formatting_text(next_child)
328 );
329 if config.newlines
330 && !inline_text_element
331 && i < element.children.len() - 1
332 && !next_is_formatting
333 {
334 output.push('\n');
335 }
336 }
337 Node::Text(t) => {
338 if config.pretty && config.newlines {
339 output.push_str(t);
340 }
341 }
342 _ => {
343 stringify_node(child, output, config, depth + 1)?;
344 }
345 }
346 }
347
348 if has_content_children
349 && config.newlines
350 && !inline_text_element
351 && !ends_with_formatting_text
352 {
353 output.push('\n');
354 if config.pretty {
355 for _ in 0..depth {
356 output.push_str(&config.indent);
357 }
358 }
359 }
360
361 output.push_str("</");
363 output.push_str(&element.name);
364 output.push('>');
365 }
366
367 Ok(())
368}
369
370fn stringify_node(
372 node: &Node,
373 output: &mut String,
374 config: &StringifyConfig,
375 depth: usize,
376) -> Result<(), VexyError> {
377 match node {
378 Node::Element(e) => stringify_element(e, output, config, depth),
379 Node::Text(t) => {
380 let should_indent = config.pretty && depth > 0 && !t.trim().is_empty();
382 if should_indent {
383 for _ in 0..depth {
384 output.push_str(&config.indent);
385 }
386 }
387 let text = if config.pretty { t.trim() } else { t.as_ref() };
388 escape_text_to(text, output);
389 Ok(())
390 }
391 Node::Comment(c) => {
392 if config.pretty && depth > 0 {
393 for _ in 0..depth {
394 output.push_str(&config.indent);
395 }
396 }
397 output.push_str("<!--");
398 output.push_str(c);
399 output.push_str("-->");
400 Ok(())
401 }
402 Node::CData(c) => {
403 output.push_str("<![CDATA[");
404 output.push_str(c);
405 output.push_str("]]>");
406 Ok(())
407 }
408 Node::ProcessingInstruction { target, data } => {
409 output.push_str("<?");
410 output.push_str(target);
411 if !data.is_empty() {
412 output.push(' ');
413 output.push_str(data);
414 }
415 output.push_str("?>");
416 Ok(())
417 }
418 Node::DocType(doctype) => {
419 output.push_str("<!DOCTYPE ");
420 output.push_str(doctype);
421 output.push('>');
422 Ok(())
423 }
424 }
425}
426
427fn escape_text_to(s: &str, output: &mut String) {
429 if !s.contains(&['&', '<', '>', '"', '\''][..]) {
431 output.push_str(s);
432 return;
433 }
434
435 output.reserve(s.len() + 10);
437
438 for ch in s.chars() {
439 match ch {
440 '&' => output.push_str("&"),
441 '<' => output.push_str("<"),
442 '>' => output.push_str(">"),
443 '"' => output.push_str("""),
444 '\'' => output.push_str("'"),
445 _ => output.push(ch),
446 }
447 }
448}
449
450#[cfg(test)]
452fn escape_attribute_to(s: &str, output: &mut String) {
453 if !s.contains(&['&', '<', '>', '"', '\''][..]) {
455 output.push_str(s);
456 return;
457 }
458
459 output.reserve(s.len() + 10);
461
462 for ch in s.chars() {
463 match ch {
464 '&' => output.push_str("&"),
465 '<' => output.push_str("<"),
466 '>' => output.push_str(">"),
467 '"' => output.push_str("""),
468 '\'' => output.push_str("'"),
469 _ => output.push(ch),
470 }
471 }
472}
473
474pub struct StreamingStringifier<W: std::io::Write> {
476 writer: W,
477 config: StringifyConfig,
478}
479
480impl<W: std::io::Write> StreamingStringifier<W> {
481 pub fn new(writer: W, config: StringifyConfig) -> Self {
483 Self { writer, config }
484 }
485
486 pub fn stringify(&mut self, document: &Document) -> Result<(), VexyError> {
488 if let Some(ref version) = document.metadata.version {
490 write!(self.writer, "<?xml version=\"{version}\"")?;
491 if let Some(ref encoding) = document.metadata.encoding {
492 write!(self.writer, " encoding=\"{encoding}\"")?;
493 }
494 if let Some(ref standalone) = document.metadata.standalone {
495 write!(self.writer, " standalone=\"{standalone}\"")?;
496 }
497 self.writer.write_all(b"?>")?;
498 if self.config.newlines {
499 self.writer.write_all(b"\n")?;
500 }
501 }
502
503 for node in &document.prologue {
505 self.stringify_node(node, 0)?;
506 if self.config.newlines && !matches!(node, Node::Text(_)) {
507 self.writer.write_all(b"\n")?;
508 }
509 }
510
511 self.stringify_element(&document.root, 0)?;
513
514 if !document.epilogue.is_empty() && self.config.newlines {
516 self.writer.write_all(b"\n")?;
517 }
518 for node in &document.epilogue {
519 self.stringify_node(node, 0)?;
520 if self.config.newlines && !matches!(node, Node::Text(_)) {
521 self.writer.write_all(b"\n")?;
522 }
523 }
524
525 self.writer.flush()?;
526 Ok(())
527 }
528
529 fn write_indent(&mut self, depth: usize) -> Result<(), VexyError> {
530 if self.config.pretty {
531 for _ in 0..depth {
532 self.writer.write_all(self.config.indent.as_bytes())?;
533 }
534 }
535 Ok(())
536 }
537
538 fn write_escaped_text(&mut self, s: &str) -> Result<(), VexyError> {
539 if !s.contains(&['&', '<', '>', '"', '\''][..]) {
540 self.writer.write_all(s.as_bytes())?;
541 return Ok(());
542 }
543
544 for ch in s.chars() {
545 match ch {
546 '&' => self.writer.write_all(b"&")?,
547 '<' => self.writer.write_all(b"<")?,
548 '>' => self.writer.write_all(b">")?,
549 '"' => self.writer.write_all(b""")?,
550 '\'' => self.writer.write_all(b"'")?,
551 _ => {
552 let mut buf = [0u8; 4];
553 let encoded = ch.encode_utf8(&mut buf);
554 self.writer.write_all(encoded.as_bytes())?;
555 }
556 }
557 }
558
559 Ok(())
560 }
561
562 fn stringify_element(&mut self, element: &Element, depth: usize) -> Result<(), VexyError> {
563 if self.config.pretty && depth > 0 {
564 self.write_indent(depth)?;
565 }
566
567 self.writer.write_all(b"<")?;
568 self.writer.write_all(element.name.as_bytes())?;
569
570 for (name, value) in &element.attributes {
571 self.writer.write_all(b" ")?;
572 self.writer.write_all(name.as_bytes())?;
573 self.writer.write_all(b"=")?;
574
575 if self.config.quote_attrs {
576 self.writer.write_all(b"\"")?;
577 self.writer.write_all(value.as_bytes())?;
578 self.writer.write_all(b"\"")?;
579 } else {
580 self.writer.write_all(value.as_bytes())?;
581 }
582 }
583
584 for (prefix, uri) in &element.namespaces {
585 let xmlns_key = if prefix.is_empty() {
586 "xmlns".to_string()
587 } else {
588 format!("xmlns:{prefix}")
589 };
590
591 if element.attributes.contains_key(xmlns_key.as_str()) {
592 continue;
593 }
594
595 self.writer.write_all(b" ")?;
596 self.writer.write_all(xmlns_key.as_bytes())?;
597 self.writer.write_all(b"=")?;
598 if self.config.quote_attrs {
599 self.writer.write_all(b"\"")?;
600 self.writer.write_all(uri.as_bytes())?;
601 self.writer.write_all(b"\"")?;
602 } else {
603 self.writer.write_all(uri.as_bytes())?;
604 }
605 }
606
607 if element.children.is_empty() && self.config.self_close {
608 self.writer.write_all(b"/>")?;
609 return Ok(());
610 }
611
612 self.writer.write_all(b">")?;
613
614 let has_element_children = element
615 .children
616 .iter()
617 .any(|n| matches!(n, Node::Element(_)));
618 let has_non_empty_text = element.children.iter().any(|n| match n {
619 Node::Text(t) => !t.trim().is_empty(),
620 _ => false,
621 });
622 let starts_with_formatting_text = matches!(
623 element.children.first(),
624 Some(Node::Text(text)) if text.trim().is_empty() && text.contains('\n')
625 );
626 let ends_with_formatting_text = matches!(
627 element.children.last(),
628 Some(Node::Text(text)) if text.trim().is_empty() && text.contains('\n')
629 );
630 let inline_text_element = element.name == "text"
631 && !has_element_children
632 && element.children.len() == 1
633 && matches!(
634 element.children.first(),
635 Some(Node::Text(t)) if !t.trim().is_empty() && !t.contains('\n') && t.as_ref() == t.trim()
636 );
637 let has_content_children = has_element_children || has_non_empty_text;
638 let is_formatting_text =
639 |node: &Node| matches!(node, Node::Text(text) if text.trim().is_empty());
640
641 if has_content_children
642 && self.config.newlines
643 && !inline_text_element
644 && !starts_with_formatting_text
645 {
646 self.writer.write_all(b"\n")?;
647 }
648
649 for (i, child) in element.children.iter().enumerate() {
650 match child {
651 Node::Element(_) => {
652 self.stringify_node(child, depth + 1)?;
653 let next_is_formatting = matches!(
654 element.children.get(i + 1),
655 Some(next_child) if is_formatting_text(next_child)
656 );
657 if self.config.newlines && i < element.children.len() - 1 && !next_is_formatting
658 {
659 self.writer.write_all(b"\n")?;
660 }
661 }
662 Node::Text(t) if !t.trim().is_empty() => {
663 let text_depth = if inline_text_element {
664 0
665 } else if element.name == "textPath" {
666 depth
667 } else {
668 depth + 1
669 };
670 self.stringify_node(child, text_depth)?;
671 let next_is_formatting = matches!(
672 element.children.get(i + 1),
673 Some(next_child) if is_formatting_text(next_child)
674 );
675 if self.config.newlines
676 && !inline_text_element
677 && i < element.children.len() - 1
678 && !next_is_formatting
679 {
680 self.writer.write_all(b"\n")?;
681 }
682 }
683 Node::Text(t) => {
684 if self.config.pretty && self.config.newlines {
685 self.writer.write_all(t.as_bytes())?;
686 }
687 }
688 _ => self.stringify_node(child, depth + 1)?,
689 }
690 }
691
692 if has_content_children
693 && self.config.newlines
694 && !inline_text_element
695 && !ends_with_formatting_text
696 {
697 self.writer.write_all(b"\n")?;
698 if self.config.pretty {
699 self.write_indent(depth)?;
700 }
701 }
702
703 self.writer.write_all(b"</")?;
704 self.writer.write_all(element.name.as_bytes())?;
705 self.writer.write_all(b">")?;
706
707 Ok(())
708 }
709
710 fn stringify_node(&mut self, node: &Node, depth: usize) -> Result<(), VexyError> {
711 match node {
712 Node::Element(e) => self.stringify_element(e, depth),
713 Node::Text(t) => {
714 let should_indent = self.config.pretty && depth > 0 && !t.trim().is_empty();
715 if should_indent {
716 self.write_indent(depth)?;
717 }
718 let text = if self.config.pretty {
719 t.trim()
720 } else {
721 t.as_ref()
722 };
723 self.write_escaped_text(text)
724 }
725 Node::Comment(c) => {
726 if self.config.pretty && depth > 0 {
727 self.write_indent(depth)?;
728 }
729 self.writer.write_all(b"<!--")?;
730 self.writer.write_all(c.as_bytes())?;
731 self.writer.write_all(b"-->")?;
732 Ok(())
733 }
734 Node::CData(c) => {
735 self.writer.write_all(b"<![CDATA[")?;
736 self.writer.write_all(c.as_bytes())?;
737 self.writer.write_all(b"]]>")?;
738 Ok(())
739 }
740 Node::ProcessingInstruction { target, data } => {
741 self.writer.write_all(b"<?")?;
742 self.writer.write_all(target.as_bytes())?;
743 if !data.is_empty() {
744 self.writer.write_all(b" ")?;
745 self.writer.write_all(data.as_bytes())?;
746 }
747 self.writer.write_all(b"?>")?;
748 Ok(())
749 }
750 Node::DocType(doctype) => {
751 self.writer.write_all(b"<!DOCTYPE ")?;
752 self.writer.write_all(doctype.as_bytes())?;
753 self.writer.write_all(b">")?;
754 Ok(())
755 }
756 }
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763 use crate::ast::Element;
764
765 #[test]
766 fn test_escape_attribute() {
767 let mut output = String::new();
768 escape_attribute_to("hello", &mut output);
769 assert_eq!(output, "hello");
770
771 output.clear();
772 escape_attribute_to("hello & world", &mut output);
773 assert_eq!(output, "hello & world");
774
775 output.clear();
776 escape_attribute_to("\"quoted\"", &mut output);
777 assert_eq!(output, ""quoted"");
778
779 output.clear();
780 escape_attribute_to("<tag>", &mut output);
781 assert_eq!(output, "<tag>");
782 }
783
784 #[test]
785 fn test_escape_text() {
786 let mut output = String::new();
787 escape_text_to("hello", &mut output);
788 assert_eq!(output, "hello");
789
790 output.clear();
791 escape_text_to("hello & world", &mut output);
792 assert_eq!(output, "hello & world");
793
794 output.clear();
795 escape_text_to("<tag>", &mut output);
796 assert_eq!(output, "<tag>");
797
798 output.clear();
799 escape_text_to("\"quoted\"", &mut output);
800 assert_eq!(output, ""quoted"");
801
802 output.clear();
803 escape_text_to("it's", &mut output);
804 assert_eq!(output, "it's");
805 }
806
807 #[test]
808 fn test_stringify_simple() {
809 let mut doc = Document::new();
810 doc.root.set_attr("width", "100");
811 doc.root.set_attr("height", "100");
812
813 let result = stringify(&doc).unwrap();
814 assert!(result.contains("<svg width=\"100\" height=\"100\"/>"));
815 }
816
817 #[test]
818 fn test_stringify_with_children() {
819 let mut doc = Document::new();
820 let mut rect = Element::new("rect");
821 rect.set_attr("x", "10");
822 rect.set_attr("y", "10");
823 doc.root.add_child(Node::Element(rect));
824
825 let result = stringify(&doc).unwrap();
826 assert!(result.contains("<svg><rect x=\"10\" y=\"10\"/></svg>"));
827 }
828
829 #[test]
830 fn test_stringify_pretty() {
831 let mut doc = Document::new();
832 let mut g = Element::new("g");
833 let mut rect = Element::new("rect");
834 rect.set_attr("x", "10");
835 g.add_child(Node::Element(rect));
836 doc.root.add_child(Node::Element(g));
837
838 let config = StringifyConfig::pretty();
839 let result = stringify_with_config(&doc, &config).unwrap();
840
841 assert!(result.contains("\n"));
842 assert!(result.contains(" <g>"));
843 assert!(result.contains(" <rect"));
844 }
845
846 #[test]
847 fn test_escape_performance() {
848 let no_escape = "simple text without special chars";
850 let mut output = String::new();
851 escape_text_to(no_escape, &mut output);
852 assert_eq!(output, no_escape);
853
854 output.clear();
856 escape_text_to("text & <tag>", &mut output);
857 assert_eq!(output, "text & <tag>");
858 }
859
860 #[test]
861 fn test_size_estimation() {
862 let mut element = Element::new("rect");
863 element.set_attr("x", "10");
864 element.set_attr("y", "20");
865 element.add_child(Node::Text("content".into()));
866
867 let estimated = estimate_element_size(&element);
868 assert!(estimated > 20); }
870
871 #[test]
872 fn test_namespace_output() {
873 let mut doc = Document::new();
874 doc.root
875 .namespaces
876 .insert("".into(), "http://www.w3.org/2000/svg".into());
877 doc.root
878 .namespaces
879 .insert("xlink".into(), "http://www.w3.org/1999/xlink".into());
880
881 let result = stringify(&doc).unwrap();
882 assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
883 assert!(result.contains("xmlns:xlink=\"http://www.w3.org/1999/xlink\""));
884 }
885
886 #[test]
887 fn test_streaming_stringifier_matches_stringify_minified() {
888 let mut doc = Document::new();
889 doc.metadata.version = Some("1.0".into());
890 doc.metadata.encoding = Some("UTF-8".into());
891 doc.metadata.standalone = Some("no".into());
892 doc.root.set_attr("width", "100");
893 doc.root.set_attr("height", "100");
894 doc.root
895 .namespaces
896 .insert("".into(), "http://www.w3.org/2000/svg".into());
897
898 let mut g = Element::new("g");
899 let mut rect = Element::new("rect");
900 rect.set_attr("x", "10");
901 rect.set_attr("y", "10");
902 rect.set_attr("fill", "red");
903 g.add_child(Node::Element(rect));
904 g.add_child(Node::Comment("note".into()));
905 doc.root.add_child(Node::Element(g));
906 doc.root
907 .add_child(Node::Text("it's \"ok\" & <safe>".into()));
908
909 let expected = stringify(&doc).unwrap();
910
911 let mut out = Vec::<u8>::new();
912 let mut streaming = StreamingStringifier::new(&mut out, StringifyConfig::default());
913 streaming.stringify(&doc).unwrap();
914 let actual = String::from_utf8(out).unwrap();
915
916 assert_eq!(actual, expected);
917 }
918
919 #[test]
920 fn test_streaming_stringifier_matches_stringify_pretty() {
921 let mut doc = Document::new();
922 let mut g = Element::new("g");
923 let mut circle = Element::new("circle");
924 circle.set_attr("cx", "5");
925 circle.set_attr("cy", "6");
926 g.add_child(Node::Element(circle));
927 doc.root.add_child(Node::Element(g));
928
929 let config = StringifyConfig::pretty();
930 let expected = stringify_with_config(&doc, &config).unwrap();
931
932 let mut out = Vec::<u8>::new();
933 let mut streaming = StreamingStringifier::new(&mut out, config);
934 streaming.stringify(&doc).unwrap();
935 let actual = String::from_utf8(out).unwrap();
936
937 assert_eq!(actual, expected);
938 }
939}