1#![deny(missing_docs)]
23
24use html_escape::{encode_double_quoted_attribute, encode_safe};
25use std::collections::HashMap;
26use std::fmt::{Display, Formatter};
27
28#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct HtmlPage {
39 head: Element,
40 body: Element,
41}
42
43impl Default for HtmlPage {
44 fn default() -> Self {
45 Self {
46 head: Element::new(Tag::Head),
47 body: Element::new(Tag::Body),
48 }
49 }
50}
51
52impl HtmlPage {
53 pub fn push_to_head(&mut self, e: impl Into<Element>) {
55 self.head.push_child(e.into());
56 }
57
58 pub fn push_to_body(&mut self, e: impl Into<Element>) {
60 self.body.push_child(e.into());
61 }
62
63 pub fn with_head_element(mut self, e: impl Into<Element>) -> Self {
65 self.head.push_child(e.into());
66 self
67 }
68
69 pub fn with_body_element(mut self, e: impl Into<Element>) -> Self {
71 self.body.push_child(e.into());
72 self
73 }
74
75 pub fn with_body_text(mut self, text: impl Into<String>) -> Self {
77 self.body.push_text(text);
78 self
79 }
80
81 pub fn push_children(&mut self, e: impl Into<Element>) {
83 for child in &e.into().children {
84 self.body.children.push(child.clone());
85 }
86 }
87}
88
89impl Display for HtmlPage {
90 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
91 writeln!(f, "<!DOCTYPE html>")?;
92 writeln!(f, "<{}>", Tag::Html)?;
93 writeln!(f, "{}", &self.head)?;
94 writeln!(f, "{}", &self.body)?;
95 writeln!(f, "</{}>", Tag::Html)?;
96 Ok(())
97 }
98}
99
100#[derive(Copy, Clone, Debug, Eq, PartialEq)]
105#[allow(missing_docs)] pub enum Tag {
108 A,
109 Abbr,
110 Address,
111 Area,
112 Article,
113 Aside,
114 Audio,
115 B,
116 Base,
117 Bdi,
118 Bdo,
119 Blockquote,
120 Body,
121 Br,
122 Button,
123 Canvas,
124 Caption,
125 Cite,
126 Code,
127 Col,
128 ColGroup,
129 Data,
130 DataList,
131 Dd,
132 Del,
133 Details,
134 Dfn,
135 Dialog,
136 Div,
137 Dl,
138 Dt,
139 Em,
140 Embed,
141 FieldSet,
142 FigCaption,
143 Figure,
144 Footer,
145 Form,
146 H1,
147 H2,
148 H3,
149 H4,
150 H5,
151 H6,
152 Head,
153 Header,
154 Hr,
155 Html,
156 I,
157 Iframe,
158 Img,
159 Input,
160 Ins,
161 Kbd,
162 Label,
163 Legend,
164 Li,
165 Link,
166 Main,
167 Map,
168 Mark,
169 Meta,
170 Meter,
171 Nav,
172 NoScript,
173 Object,
174 Ol,
175 OptGroup,
176 Option,
177 Output,
178 P,
179 Param,
180 Picture,
181 Pre,
182 Progress,
183 Q,
184 Rp,
185 Rt,
186 Ruby,
187 S,
188 Samp,
189 Script,
190 Section,
191 Select,
192 Small,
193 Source,
194 Span,
195 Strong,
196 Style,
197 Sub,
198 Summary,
199 Sup,
200 Svg,
201 Table,
202 Tbody,
203 Td,
204 Template,
205 TextArea,
206 Tfoot,
207 Th,
208 Time,
209 Title,
210 Tr,
211 Track,
212 U,
213 Ul,
214 Var,
215 Video,
216 Wbr,
217}
218
219impl Display for Tag {
220 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
221 write!(f, "{}", self.as_str())
222 }
223}
224
225impl Tag {
226 fn as_str(&self) -> &str {
227 match self {
228 Self::A => "A",
229 Self::Abbr => "ABBR",
230 Self::Address => "ADDRESS",
231 Self::Area => "AREA",
232 Self::Article => "ARTICLE",
233 Self::Aside => "ASIDE",
234 Self::Audio => "AUDIO",
235 Self::B => "B",
236 Self::Base => "BASE",
237 Self::Bdi => "BDI",
238 Self::Bdo => "BDO",
239 Self::Blockquote => "BLOCKQUOTE",
240 Self::Body => "BODY",
241 Self::Br => "BR",
242 Self::Button => "BUTTON",
243 Self::Canvas => "CANVAS",
244 Self::Caption => "CAPTION",
245 Self::Cite => "CITE",
246 Self::Code => "CODE",
247 Self::Col => "COL",
248 Self::ColGroup => "COLGROUP",
249 Self::Data => "DATA",
250 Self::DataList => "DATALIST",
251 Self::Dd => "DD",
252 Self::Del => "DEL",
253 Self::Details => "DETAILS",
254 Self::Dfn => "DFN",
255 Self::Dialog => "DIALOG",
256 Self::Div => "DIV",
257 Self::Dl => "DL",
258 Self::Dt => "DT",
259 Self::Em => "EM",
260 Self::Embed => "EMBED",
261 Self::FieldSet => "FIELDSET",
262 Self::FigCaption => "FIGCAPTIO",
263 Self::Figure => "FIGURE",
264 Self::Footer => "FOOTER",
265 Self::Form => "FORM",
266 Self::H1 => "H1",
267 Self::H2 => "H2",
268 Self::H3 => "H3",
269 Self::H4 => "H4",
270 Self::H5 => "H5",
271 Self::H6 => "H6",
272 Self::Head => "HEAD",
273 Self::Header => "HEADER",
274 Self::Hr => "HR",
275 Self::Html => "HTML",
276 Self::I => "I",
277 Self::Iframe => "IFRAME",
278 Self::Img => "IMG",
279 Self::Input => "INPUT",
280 Self::Ins => "INS",
281 Self::Kbd => "KBD",
282 Self::Label => "LABEL",
283 Self::Legend => "LEGEND",
284 Self::Li => "LI",
285 Self::Link => "LINK",
286 Self::Main => "MAIN",
287 Self::Map => "MAP",
288 Self::Mark => "MARK",
289 Self::Meta => "META",
290 Self::Meter => "METER",
291 Self::Nav => "NAV",
292 Self::NoScript => "NOSCRIPT",
293 Self::Object => "OBJECT",
294 Self::Ol => "OL",
295 Self::OptGroup => "OPTGROUP",
296 Self::Option => "OPTION",
297 Self::Output => "OUTPUT",
298 Self::P => "P",
299 Self::Param => "PARAM",
300 Self::Picture => "PICTURE",
301 Self::Pre => "PRE",
302 Self::Progress => "PROGRESS",
303 Self::Q => "Q",
304 Self::Rp => "RP",
305 Self::Rt => "RT",
306 Self::Ruby => "RUBY",
307 Self::S => "S",
308 Self::Samp => "SAMP",
309 Self::Script => "SCRIPT",
310 Self::Section => "SECTION",
311 Self::Select => "SELECT",
312 Self::Small => "SMALL",
313 Self::Source => "SOURCE",
314 Self::Span => "SPAN",
315 Self::Strong => "STRONG",
316 Self::Style => "STYLE",
317 Self::Sub => "SUB",
318 Self::Summary => "SUMMARY",
319 Self::Sup => "SUP",
320 Self::Svg => "SVG",
321 Self::Table => "TABLE",
322 Self::Tbody => "TBODY",
323 Self::Td => "TD",
324 Self::Template => "TEMPLATE",
325 Self::TextArea => "TEXTAREA",
326 Self::Tfoot => "TFOOT",
327 Self::Th => "TH",
328 Self::Time => "TIME",
329 Self::Title => "TITLE",
330 Self::Tr => "TR",
331 Self::Track => "TRACK",
332 Self::U => "U",
333 Self::Ul => "UL",
334 Self::Var => "VAR",
335 Self::Video => "VIDEO",
336 Self::Wbr => "WBR",
337 }
338 }
339
340 fn can_self_close(&self) -> bool {
341 matches!(
342 self,
343 Self::Area
344 | Self::Base
345 | Self::Br
346 | Self::Col
347 | Self::Embed
348 | Self::Hr
349 | Self::Img
350 | Self::Input
351 | Self::Link
352 | Self::Meta
353 | Self::Param
354 | Self::Source
355 | Self::Track
356 | Self::Wbr
357 )
358 }
359}
360
361#[cfg(test)]
362mod test_tag {
363 use super::Tag;
364
365 #[test]
366 fn can_self_close() {
367 assert!(Tag::Area.can_self_close());
368 assert!(Tag::Base.can_self_close());
369 assert!(Tag::Br.can_self_close());
370 assert!(Tag::Col.can_self_close());
371 assert!(Tag::Embed.can_self_close());
372 assert!(Tag::Hr.can_self_close());
373 assert!(Tag::Img.can_self_close());
374 assert!(Tag::Input.can_self_close());
375 assert!(Tag::Link.can_self_close());
376 assert!(Tag::Meta.can_self_close());
377 assert!(Tag::Param.can_self_close());
378 assert!(Tag::Source.can_self_close());
379 assert!(Tag::Track.can_self_close());
380 assert!(Tag::Wbr.can_self_close());
381 }
382}
383
384#[derive(Clone, Debug, Default, Eq, PartialEq)]
385struct Attributes {
386 attrs: HashMap<String, AttributeValue>,
387}
388
389impl Attributes {
390 fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
391 self.attrs
392 .insert(name.into(), AttributeValue::String(value.into()));
393 }
394
395 fn set_boolean(&mut self, name: impl Into<String>) {
396 self.attrs.insert(name.into(), AttributeValue::Boolean);
397 }
398
399 fn unset(&mut self, name: impl AsRef<str>) {
400 self.attrs.remove(name.as_ref());
401 }
402
403 fn get(&self, name: impl AsRef<str>) -> Option<&AttributeValue> {
404 self.attrs.get(name.as_ref())
405 }
406
407 fn names(&self) -> impl Iterator<Item = &str> {
408 self.attrs.keys().map(|s| s.as_ref())
409 }
410}
411
412impl Display for Attributes {
413 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
414 for (name, value) in self.attrs.iter() {
415 match value {
416 AttributeValue::Boolean => write!(f, " {name}")?,
417 AttributeValue::String(s) => {
418 write!(f, " {}=\"{}\"", name, encode_double_quoted_attribute(s))?
419 }
420 }
421 }
422 Ok(())
423 }
424}
425
426#[derive(Clone, Debug, Eq, PartialEq)]
435pub enum AttributeValue {
436 String(String),
438 Boolean,
441}
442
443impl AttributeValue {
444 pub fn as_str(&self) -> &str {
447 match self {
448 Self::String(s) => s,
449 Self::Boolean => "",
450 }
451 }
452}
453
454#[derive(Clone, Debug, Eq, PartialEq)]
460pub struct Element {
461 loc: Option<(usize, usize)>,
462 tag: Tag,
463 attrs: Attributes,
464 children: Vec<Content>,
465}
466
467impl Element {
468 pub fn new(tag: Tag) -> Self {
470 Self {
471 tag,
472 attrs: Attributes::default(),
473 children: vec![],
474 loc: None,
475 }
476 }
477
478 pub fn p() -> Self {
480 Self::new(Tag::P)
481 }
482
483 pub fn span() -> Self {
485 Self::new(Tag::Span)
486 }
487
488 pub fn div() -> Self {
490 Self::new(Tag::Div)
491 }
492
493 pub fn code() -> Self {
495 Self::new(Tag::Code)
496 }
497
498 pub fn h1() -> Self {
500 Self::new(Tag::H1)
501 }
502
503 pub fn h2() -> Self {
505 Self::new(Tag::H2)
506 }
507
508 pub fn h3() -> Self {
510 Self::new(Tag::H3)
511 }
512
513 pub fn with_location(mut self, line: usize, col: usize) -> Self {
515 self.loc = Some((line, col));
516 self
517 }
518
519 pub fn with_child(mut self, child: Element) -> Self {
521 self.children.push(Content::Element(child));
522 self
523 }
524
525 pub fn with_text(mut self, child: impl Into<String>) -> Self {
527 self.children.push(Content::Text(child.into()));
528 self
529 }
530
531 pub fn with_attribute(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
533 self.attrs.set(name.as_ref(), value.as_ref());
534 self
535 }
536
537 pub fn with_boolean_attribute(mut self, name: impl Into<String>) -> Self {
539 self.attrs.set_boolean(name);
540 self
541 }
542
543 pub fn with_class(mut self, class: impl Into<String>) -> Self {
545 self.add_class(class);
546 self
547 }
548
549 pub fn tag(&self) -> Tag {
551 self.tag
552 }
553
554 pub fn location(&self) -> Option<(usize, usize)> {
556 self.loc
557 }
558
559 pub fn children(&self) -> &[Content] {
561 &self.children
562 }
563
564 pub fn attributes(&self) -> impl Iterator<Item = &str> {
566 self.attrs.names()
567 }
568
569 pub fn attribute(&self, name: impl AsRef<str>) -> Option<&AttributeValue> {
572 self.attrs.get(name.as_ref())
573 }
574
575 pub fn attribute_value(&self, name: impl AsRef<str>) -> Option<&str> {
578 self.attrs.get(name.as_ref()).map(|v| v.as_str())
579 }
580
581 pub fn set_attribute(&mut self, name: impl Into<String>, value: impl Into<String>) {
584 self.attrs.set(name, value);
585 }
586
587 pub fn set_boolean_attribute(&mut self, name: impl Into<String>) {
589 self.attrs.set_boolean(name);
590 }
591
592 pub fn unset_attribute(&mut self, name: impl AsRef<str>) {
594 self.attrs.unset(name.as_ref());
595 }
596
597 pub fn classes(&self) -> impl Iterator<Item = &str> {
599 let v = self.attribute_value("class").unwrap_or_default();
600 v.split_ascii_whitespace()
601 }
602
603 pub fn has_class(&self, wanted: impl AsRef<str>) -> bool {
605 let wanted = wanted.as_ref();
606 self.classes().any(|v| v == wanted)
607 }
608
609 pub fn add_class(&mut self, class: impl Into<String>) {
612 let class = class.into();
613 if let Some(old) = self.attribute_value("class") {
614 if !old.split_ascii_whitespace().any(|s| s == class) {
615 self.set_attribute("class", format!("{old} {class}"));
616 }
617 } else {
618 self.set_attribute("class", class);
619 }
620 }
621
622 pub fn push_text(&mut self, text: impl Into<String>) {
625 self.children.push(Content::text(text));
626 }
627
628 pub fn push_child(&mut self, child: Element) {
630 self.children.push(Content::element(&child));
631 }
632
633 pub fn push_children(&mut self, children: &[Element]) {
635 for child in children {
636 self.children.push(Content::element(child));
637 }
638 }
639
640 pub fn clear_children(&mut self) {
642 self.children.clear();
643 }
644
645 pub fn push_html(&mut self, html: impl Into<String>) {
650 self.children.push(Content::html(html));
651 }
652
653 pub fn serialize(&self) -> String {
655 format!("{self}")
656 }
657
658 pub fn plain_text(&self) -> String {
661 let mut text = TextVisitor::default();
662 text.visit(self);
663 text.text
664 }
665}
666
667impl Display for Element {
668 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
669 if self.tag().can_self_close() && self.children.is_empty() {
670 write!(f, "<{}{}/>", self.tag, self.attrs)?;
671 } else {
672 write!(f, "<{}{}>", self.tag, self.attrs)?;
673 for child in &self.children {
674 write!(f, "{child}")?;
675 }
676 write!(f, "</{}>", self.tag)?;
677 }
678 Ok(())
679 }
680}
681
682impl From<&Element> for Element {
683 fn from(value: &Element) -> Self {
684 value.clone()
685 }
686}
687
688#[cfg(test)]
689mod test_element {
690 use super::{Element, Tag};
691
692 #[test]
693 fn empty_p() {
694 let e = Element::new(Tag::P);
695 assert_eq!(e.to_string(), "<P></P>");
696 }
697
698 #[test]
699 fn empty_br() {
700 let e = Element::new(Tag::Br);
701 assert_eq!(e.to_string(), "<BR/>");
702 }
703}
704
705#[derive(Clone, Debug, Eq, PartialEq)]
707pub enum Content {
708 Text(String),
710 Element(Element),
712 Html(String),
714}
715
716impl Content {
717 pub fn text(s: impl Into<String>) -> Self {
719 Self::Text(s.into())
720 }
721
722 pub fn element(e: &Element) -> Self {
724 Self::Element(e.clone())
725 }
726
727 pub fn html(s: impl Into<String>) -> Self {
729 Self::Html(s.into())
730 }
731}
732
733impl Display for Content {
734 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
735 match self {
736 Self::Text(s) => write!(f, "{}", encode_safe(s))?,
737 Self::Element(e) => write!(f, "{e}")?,
738 Self::Html(s) => write!(f, "{s}")?,
739 }
740 Ok(())
741 }
742}
743
744pub trait Visitor {
781 fn visit_element(&mut self, _: &Element) {}
783 fn visit_text(&mut self, _: &str) {}
785 fn visit_html(&mut self, _: &str) {}
787
788 fn visit(&mut self, root: &Element) {
790 self.visit_element(root);
791 for child in &root.children {
792 match child {
793 Content::Text(s) => self.visit_text(s),
794 Content::Element(e) => self.visit(e),
795 Content::Html(s) => self.visit_html(s),
796 }
797 }
798 }
799}
800
801#[derive(Debug, Default)]
815pub struct TextVisitor {
816 pub text: String,
818}
819
820impl Visitor for TextVisitor {
821 fn visit_text(&mut self, s: &str) {
822 self.text.push_str(s);
823 }
824}
825
826#[cfg(test)]
827mod test {
828 use super::{AttributeValue, Content, Element, Tag, Visitor};
829
830 #[test]
831 fn element_has_correct_tag() {
832 let e = Element::new(Tag::P);
833 assert_eq!(e.tag(), Tag::P);
834 }
835
836 #[test]
837 fn element_has_no_attributes_initially() {
838 let e = Element::new(Tag::P);
839 assert_eq!(e.attributes().count(), 0);
840 }
841
842 #[test]
843 fn element_returns_no_value_for_missing_attribute() {
844 let e = Element::new(Tag::P);
845 assert_eq!(e.attribute("foo"), None);
846 }
847
848 #[test]
849 fn can_add_attribute_to_element() {
850 let mut e = Element::new(Tag::P);
851 e.set_attribute("foo", "bar");
852 assert_eq!(
853 e.attribute("foo"),
854 Some(&AttributeValue::String("bar".into()))
855 );
856 assert_eq!(e.attribute("foo").map(|x| x.as_str()), Some("bar"));
857 assert_eq!(e.attribute_value("foo"), Some("bar"));
858 }
859
860 #[test]
861 fn can_create_element_with_attribute() {
862 let e = Element::new(Tag::P).with_attribute("foo", "bar");
863 assert_eq!(
864 e.attribute("foo"),
865 Some(&AttributeValue::String("bar".into()))
866 );
867 }
868
869 #[test]
870 fn can_add_class_to_element() {
871 let mut e = Element::new(Tag::P);
872 e.add_class("foo");
873 let classes: Vec<&str> = e.classes().collect();
874 assert_eq!(classes, ["foo"]);
875 assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
876 }
877
878 #[test]
879 fn can_two_classes_to_element() {
880 let mut e = Element::new(Tag::P);
881 e.add_class("foo");
882 e.add_class("bar");
883 let classes: Vec<&str> = e.classes().collect();
884 assert_eq!(classes, ["foo", "bar"]);
885 assert_eq!(e.to_string(), r#"<P class="foo bar"></P>"#);
886 }
887
888 #[test]
889 fn can_add_same_class_twice_to_element() {
890 let mut e = Element::new(Tag::P);
891 e.add_class("foo");
892 e.add_class("foo");
893 let classes: Vec<&str> = e.classes().collect();
894 assert_eq!(classes, ["foo"]);
895 assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
896 }
897
898 #[test]
899 fn can_add_boolean_attribute_to_element() {
900 let mut e = Element::new(Tag::P);
901 e.set_boolean_attribute("foo");
902 assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
903 }
904
905 #[test]
906 fn can_create_element_with_boolan_attribute() {
907 let e = Element::new(Tag::P).with_boolean_attribute("foo");
908 assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
909 }
910
911 #[test]
912 fn unset_attribute_is_unset() {
913 let e = Element::new(Tag::P);
914 assert_eq!(e.attribute("foo"), None);
915 }
916
917 #[test]
918 fn can_unset_attribute_in_element() {
919 let mut e = Element::new(Tag::P);
920 e.set_attribute("foo", "bar");
921 e.unset_attribute("foo");
922 assert_eq!(e.attribute("foo"), None);
923 }
924
925 #[test]
926 fn element_has_no_children_initially() {
927 let e = Element::new(Tag::P);
928 assert!(e.children.is_empty());
929 }
930
931 #[test]
932 fn add_child_to_element() {
933 let mut e = Element::new(Tag::P);
934 let child = Content::text("foo");
935 e.push_text("foo");
936 assert_eq!(e.children(), &[child]);
937 }
938
939 #[test]
940 fn element_has_no_location_initially() {
941 let e = Element::new(Tag::P);
942 assert!(e.location().is_none());
943 }
944
945 #[test]
946 fn element_with_location() {
947 let e = Element::new(Tag::P).with_location(1, 2);
948 assert_eq!(e.location(), Some((1, 2)));
949 }
950
951 #[test]
952 fn attribute_can_be_serialized() {
953 let mut e = Element::new(Tag::P);
954 e.set_attribute("foo", "bar");
955 assert_eq!(e.serialize(), "<P foo=\"bar\"></P>");
956 }
957
958 #[test]
959 fn dangerous_attribute_value_is_esacped() {
960 let mut e = Element::new(Tag::P);
961 e.set_attribute("foo", "<");
962 assert_eq!(e.serialize(), "<P foo=\"<\"></P>");
963 }
964
965 #[test]
966 fn boolean_attribute_can_be_serialized() {
967 let mut e = Element::new(Tag::P);
968 e.set_boolean_attribute("foo");
969 assert_eq!(e.serialize(), "<P foo></P>");
970 }
971
972 #[test]
973 fn element_can_be_serialized() {
974 let mut e = Element::new(Tag::P);
975 e.push_text("hello ");
976 let mut world = Element::new(Tag::B);
977 world.push_text("world");
978 e.push_child(world);
979 assert_eq!(e.serialize(), "<P>hello <B>world</B></P>");
980 }
981
982 #[test]
983 fn dangerous_text_is_escaped() {
984 let mut e = Element::new(Tag::P);
985 e.push_text("hello <world>");
986 assert_eq!(e.serialize(), "<P>hello <world></P>");
987 }
988
989 #[test]
990 fn element_has_no_class_initially() {
991 let e = Element::new(Tag::P);
992 assert_eq!(e.attribute_value("class"), None);
993 assert_eq!(e.classes().next(), None);
994 assert!(!e.has_class("foo"));
995 }
996
997 #[test]
998 fn element_adds_first_class() {
999 let mut e = Element::new(Tag::P);
1000 e.add_class("foo");
1001 assert_eq!(e.attribute_value("class"), Some("foo"));
1002 assert!(e.has_class("foo"));
1003 }
1004
1005 #[test]
1006 fn element_adds_second_class() {
1007 let mut e = Element::new(Tag::P);
1008 e.add_class("foo");
1009 e.add_class("bar");
1010 assert_eq!(e.attribute_value("class"), Some("foo bar"));
1011 assert!(e.has_class("foo"));
1012 assert!(e.has_class("bar"));
1013 }
1014
1015 #[test]
1016 fn creates_classy_element() {
1017 let e = Element::new(Tag::P).with_class("foo").with_class("bar");
1018 assert_eq!(e.attribute_value("class"), Some("foo bar"));
1019 assert!(e.has_class("foo"));
1020 assert!(e.has_class("bar"));
1021 }
1022
1023 #[derive(Default)]
1024 struct Collector {
1025 tags: Vec<Tag>,
1026 text: String,
1027 }
1028
1029 impl Visitor for Collector {
1030 fn visit_element(&mut self, e: &Element) {
1031 self.tags.push(e.tag());
1032 }
1033
1034 fn visit_text(&mut self, s: &str) {
1035 self.text.push_str(s);
1036 }
1037 }
1038
1039 #[test]
1040 fn visits_all_children() {
1041 let e = Element::new(Tag::P)
1042 .with_text("hello ")
1043 .with_child(Element::new(Tag::B).with_text("world"));
1044
1045 let mut collector = Collector::default();
1046 collector.visit(&e);
1047 assert_eq!(collector.tags, vec![Tag::P, Tag::B]);
1048 assert_eq!(collector.text, "hello world");
1049 }
1050}