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