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: Element) {
55 self.head.push_child(e);
56 }
57
58 pub fn push_to_body(&mut self, e: Element) {
60 self.body.push_child(e);
61 }
62
63 pub fn with_head_element(mut self, e: Element) -> Self {
65 self.head.push_child(e);
66 self
67 }
68
69 pub fn with_body_element(mut self, e: Element) -> Self {
71 self.body.push_child(e);
72 self
73 }
74
75 pub fn with_body_text(mut self, text: &str) -> Self {
77 self.body.push_text(text);
78 self
79 }
80
81 pub fn push_children(&mut self, e: &Element) {
83 for child in &e.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 match self {
342 Self::Area
343 | Self::Base
344 | Self::Br
345 | Self::Col
346 | Self::Embed
347 | Self::Hr
348 | Self::Img
349 | Self::Input
350 | Self::Link
351 | Self::Meta
352 | Self::Param
353 | Self::Source
354 | Self::Track
355 | Self::Wbr => true,
356 _ => false,
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: &str, value: &str) {
391 self.attrs
392 .insert(name.into(), AttributeValue::String(value.into()));
393 }
394
395 fn set_boolean(&mut self, name: &str) {
396 self.attrs.insert(name.into(), AttributeValue::Boolean);
397 }
398
399 fn unset(&mut self, name: &str) {
400 self.attrs.remove(name);
401 }
402
403 fn get(&self, name: &str) -> Option<&AttributeValue> {
404 self.attrs.get(name)
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 with_location(mut self, line: usize, col: usize) -> Self {
480 self.loc = Some((line, col));
481 self
482 }
483
484 pub fn with_child(mut self, child: Element) -> Self {
486 self.children.push(Content::Element(child));
487 self
488 }
489
490 pub fn with_text(mut self, child: &str) -> Self {
492 self.children.push(Content::Text(child.into()));
493 self
494 }
495
496 pub fn with_attribute(mut self, name: &str, value: &str) -> Self {
498 self.attrs.set(name, value);
499 self
500 }
501
502 pub fn with_boolean_attribute(mut self, name: &str) -> Self {
504 self.attrs.set_boolean(name);
505 self
506 }
507
508 pub fn with_class(mut self, class: &str) -> Self {
510 self.add_class(class);
511 self
512 }
513
514 pub fn tag(&self) -> Tag {
516 self.tag
517 }
518
519 pub fn location(&self) -> Option<(usize, usize)> {
521 self.loc
522 }
523
524 pub fn attributes(&self) -> impl Iterator<Item = &str> {
526 self.attrs.names()
527 }
528
529 pub fn attribute(&self, name: &str) -> Option<&AttributeValue> {
532 self.attrs.get(name)
533 }
534
535 pub fn attribute_value(&self, name: &str) -> Option<&str> {
538 self.attrs.get(name).map(|v| v.as_str())
539 }
540
541 pub fn set_attribute(&mut self, name: &str, value: &str) {
544 self.attrs.set(name, value);
545 }
546
547 pub fn set_boolean_attribute(&mut self, name: &str) {
549 self.attrs.set_boolean(name);
550 }
551
552 pub fn unset_attribute(&mut self, name: &str) {
554 self.attrs.unset(name);
555 }
556
557 pub fn classes(&self) -> impl Iterator<Item = &str> {
559 let v = if let Some(v) = self.attribute_value("class") {
560 v
561 } else {
562 ""
563 };
564 v.split_ascii_whitespace()
565 }
566
567 pub fn has_class(&self, wanted: &str) -> bool {
569 self.classes().any(|v| v == wanted)
570 }
571
572 pub fn add_class(&mut self, class: &str) {
575 if let Some(old) = self.attribute_value("class") {
576 if !old.split_ascii_whitespace().any(|s| s == class) {
577 self.set_attribute("class", &format!("{old} {class}"));
578 }
579 } else {
580 self.set_attribute("class", class);
581 }
582 }
583
584 pub fn push_text(&mut self, text: &str) {
587 self.children.push(Content::text(text));
588 }
589
590 pub fn push_child(&mut self, child: Element) {
592 self.children.push(Content::element(&child));
593 }
594
595 pub fn clear_children(&mut self) {
597 self.children.clear();
598 }
599
600 pub fn push_html(&mut self, html: &str) {
605 self.children.push(Content::html(html));
606 }
607
608 pub fn serialize(&self) -> String {
610 format!("{}", self)
611 }
612
613 pub fn plain_text(&self) -> String {
616 let mut text = TextVisitor::default();
617 text.visit(self);
618 text.text
619 }
620}
621
622impl Display for Element {
623 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
624 if self.tag().can_self_close() && self.children.is_empty() {
625 write!(f, "<{}{}/>", self.tag, self.attrs)?;
626 } else {
627 write!(f, "<{}{}>", self.tag, self.attrs)?;
628 for child in &self.children {
629 write!(f, "{}", child)?;
630 }
631 write!(f, "</{}>", self.tag)?;
632 }
633 Ok(())
634 }
635}
636
637#[cfg(test)]
638mod test_element {
639 use super::{Element, Tag};
640
641 #[test]
642 fn empty_p() {
643 let e = Element::new(Tag::P);
644 assert_eq!(e.to_string(), "<P></P>");
645 }
646
647 #[test]
648 fn empty_br() {
649 let e = Element::new(Tag::Br);
650 assert_eq!(e.to_string(), "<BR/>");
651 }
652}
653
654#[derive(Clone, Debug, Eq, PartialEq)]
656pub enum Content {
657 Text(String),
659 Element(Element),
661 Html(String),
663}
664
665impl Content {
666 pub fn text(s: &str) -> Self {
668 Self::Text(s.into())
669 }
670
671 pub fn element(e: &Element) -> Self {
673 Self::Element(e.clone())
674 }
675
676 pub fn html(s: &str) -> Self {
678 Self::Html(s.into())
679 }
680}
681
682impl Display for Content {
683 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
684 match self {
685 Self::Text(s) => write!(f, "{}", encode_safe(s))?,
686 Self::Element(e) => write!(f, "{}", e)?,
687 Self::Html(s) => write!(f, "{}", s)?,
688 }
689 Ok(())
690 }
691}
692
693pub trait Visitor {
730 fn visit_element(&mut self, _: &Element) {}
732 fn visit_text(&mut self, _: &str) {}
734 fn visit_html(&mut self, _: &str) {}
736
737 fn visit(&mut self, root: &Element) {
739 self.visit_element(root);
740 for child in &root.children {
741 match child {
742 Content::Text(s) => self.visit_text(s),
743 Content::Element(e) => self.visit(e),
744 Content::Html(s) => self.visit_html(s),
745 }
746 }
747 }
748}
749
750#[derive(Debug, Default)]
764pub struct TextVisitor {
765 pub text: String,
767}
768
769impl Visitor for TextVisitor {
770 fn visit_text(&mut self, s: &str) {
771 self.text.push_str(s);
772 }
773}
774
775#[cfg(test)]
776mod test {
777 use super::{AttributeValue, Content, Element, Tag, Visitor};
778
779 #[test]
780 fn element_has_correct_tag() {
781 let e = Element::new(Tag::P);
782 assert_eq!(e.tag(), Tag::P);
783 }
784
785 #[test]
786 fn element_has_no_attributes_initially() {
787 let e = Element::new(Tag::P);
788 assert_eq!(e.attributes().count(), 0);
789 }
790
791 #[test]
792 fn element_returns_no_value_for_missing_attribute() {
793 let e = Element::new(Tag::P);
794 assert_eq!(e.attribute("foo"), None);
795 }
796
797 #[test]
798 fn can_add_attribute_to_element() {
799 let mut e = Element::new(Tag::P);
800 e.set_attribute("foo", "bar");
801 assert_eq!(
802 e.attribute("foo"),
803 Some(&AttributeValue::String("bar".into()))
804 );
805 assert_eq!(e.attribute("foo").map(|x| x.as_str()), Some("bar"));
806 assert_eq!(e.attribute_value("foo"), Some("bar"));
807 }
808
809 #[test]
810 fn can_create_element_with_attribute() {
811 let e = Element::new(Tag::P).with_attribute("foo", "bar");
812 assert_eq!(
813 e.attribute("foo"),
814 Some(&AttributeValue::String("bar".into()))
815 );
816 }
817
818 #[test]
819 fn can_add_class_to_element() {
820 let mut e = Element::new(Tag::P);
821 e.add_class("foo");
822 let classes: Vec<&str> = e.classes().collect();
823 assert_eq!(classes, ["foo"]);
824 assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
825 }
826
827 #[test]
828 fn can_two_classes_to_element() {
829 let mut e = Element::new(Tag::P);
830 e.add_class("foo");
831 e.add_class("bar");
832 let classes: Vec<&str> = e.classes().collect();
833 assert_eq!(classes, ["foo", "bar"]);
834 assert_eq!(e.to_string(), r#"<P class="foo bar"></P>"#);
835 }
836
837 #[test]
838 fn can_add_same_class_twice_to_element() {
839 let mut e = Element::new(Tag::P);
840 e.add_class("foo");
841 e.add_class("foo");
842 let classes: Vec<&str> = e.classes().collect();
843 assert_eq!(classes, ["foo"]);
844 assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
845 }
846
847 #[test]
848 fn can_add_boolean_attribute_to_element() {
849 let mut e = Element::new(Tag::P);
850 e.set_boolean_attribute("foo");
851 assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
852 }
853
854 #[test]
855 fn can_create_element_with_boolan_attribute() {
856 let e = Element::new(Tag::P).with_boolean_attribute("foo");
857 assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
858 }
859
860 #[test]
861 fn unset_attribute_is_unset() {
862 let e = Element::new(Tag::P);
863 assert_eq!(e.attribute("foo"), None);
864 }
865
866 #[test]
867 fn can_unset_attribute_in_element() {
868 let mut e = Element::new(Tag::P);
869 e.set_attribute("foo", "bar");
870 e.unset_attribute("foo");
871 assert_eq!(e.attribute("foo"), None);
872 }
873
874 #[test]
875 fn element_has_no_children_initially() {
876 let e = Element::new(Tag::P);
877 assert!(e.children.is_empty());
878 }
879
880 #[test]
881 fn add_child_to_element() {
882 let mut e = Element::new(Tag::P);
883 let child = Content::text("foo");
884 e.push_text("foo");
885 assert_eq!(e.children, &[child]);
886 }
887
888 #[test]
889 fn element_has_no_location_initially() {
890 let e = Element::new(Tag::P);
891 assert!(e.location().is_none());
892 }
893
894 #[test]
895 fn element_with_location() {
896 let e = Element::new(Tag::P).with_location(1, 2);
897 assert_eq!(e.location(), Some((1, 2)));
898 }
899
900 #[test]
901 fn attribute_can_be_serialized() {
902 let mut e = Element::new(Tag::P);
903 e.set_attribute("foo", "bar");
904 assert_eq!(e.serialize(), "<P foo=\"bar\"></P>");
905 }
906
907 #[test]
908 fn dangerous_attribute_value_is_esacped() {
909 let mut e = Element::new(Tag::P);
910 e.set_attribute("foo", "<");
911 assert_eq!(e.serialize(), "<P foo=\"<\"></P>");
912 }
913
914 #[test]
915 fn boolean_attribute_can_be_serialized() {
916 let mut e = Element::new(Tag::P);
917 e.set_boolean_attribute("foo");
918 assert_eq!(e.serialize(), "<P foo></P>");
919 }
920
921 #[test]
922 fn element_can_be_serialized() {
923 let mut e = Element::new(Tag::P);
924 e.push_text("hello ");
925 let mut world = Element::new(Tag::B);
926 world.push_text("world");
927 e.push_child(world);
928 assert_eq!(e.serialize(), "<P>hello <B>world</B></P>");
929 }
930
931 #[test]
932 fn dangerous_text_is_escaped() {
933 let mut e = Element::new(Tag::P);
934 e.push_text("hello <world>");
935 assert_eq!(e.serialize(), "<P>hello <world></P>");
936 }
937
938 #[test]
939 fn element_has_no_class_initially() {
940 let e = Element::new(Tag::P);
941 assert_eq!(e.attribute_value("class"), None);
942 assert_eq!(e.classes().next(), None);
943 assert!(!e.has_class("foo"));
944 }
945
946 #[test]
947 fn element_adds_first_class() {
948 let mut e = Element::new(Tag::P);
949 e.add_class("foo");
950 assert_eq!(e.attribute_value("class"), Some("foo"));
951 assert!(e.has_class("foo"));
952 }
953
954 #[test]
955 fn element_adds_second_class() {
956 let mut e = Element::new(Tag::P);
957 e.add_class("foo");
958 e.add_class("bar");
959 assert_eq!(e.attribute_value("class"), Some("foo bar"));
960 assert!(e.has_class("foo"));
961 assert!(e.has_class("bar"));
962 }
963
964 #[test]
965 fn creates_classy_element() {
966 let e = Element::new(Tag::P).with_class("foo").with_class("bar");
967 assert_eq!(e.attribute_value("class"), Some("foo bar"));
968 assert!(e.has_class("foo"));
969 assert!(e.has_class("bar"));
970 }
971
972 #[derive(Default)]
973 struct Collector {
974 tags: Vec<Tag>,
975 text: String,
976 }
977
978 impl Visitor for Collector {
979 fn visit_element(&mut self, e: &Element) {
980 self.tags.push(e.tag());
981 }
982
983 fn visit_text(&mut self, s: &str) {
984 self.text.push_str(s);
985 }
986 }
987
988 #[test]
989 fn visits_all_children() {
990 let e = Element::new(Tag::P)
991 .with_text("hello ")
992 .with_child(Element::new(Tag::B).with_text("world"));
993
994 let mut collector = Collector::default();
995 collector.visit(&e);
996 assert_eq!(collector.tags, vec![Tag::P, Tag::B]);
997 assert_eq!(collector.text, "hello world");
998 }
999}