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 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: &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 children(&self) -> &[Content] {
526 &self.children
527 }
528
529 pub fn attributes(&self) -> impl Iterator<Item = &str> {
531 self.attrs.names()
532 }
533
534 pub fn attribute(&self, name: &str) -> Option<&AttributeValue> {
537 self.attrs.get(name)
538 }
539
540 pub fn attribute_value(&self, name: &str) -> Option<&str> {
543 self.attrs.get(name).map(|v| v.as_str())
544 }
545
546 pub fn set_attribute(&mut self, name: &str, value: &str) {
549 self.attrs.set(name, value);
550 }
551
552 pub fn set_boolean_attribute(&mut self, name: &str) {
554 self.attrs.set_boolean(name);
555 }
556
557 pub fn unset_attribute(&mut self, name: &str) {
559 self.attrs.unset(name);
560 }
561
562 pub fn classes(&self) -> impl Iterator<Item = &str> {
564 let v = self.attribute_value("class").unwrap_or_default();
565 v.split_ascii_whitespace()
566 }
567
568 pub fn has_class(&self, wanted: &str) -> bool {
570 self.classes().any(|v| v == wanted)
571 }
572
573 pub fn add_class(&mut self, class: &str) {
576 if let Some(old) = self.attribute_value("class") {
577 if !old.split_ascii_whitespace().any(|s| s == class) {
578 self.set_attribute("class", &format!("{old} {class}"));
579 }
580 } else {
581 self.set_attribute("class", class);
582 }
583 }
584
585 pub fn push_text(&mut self, text: &str) {
588 self.children.push(Content::text(text));
589 }
590
591 pub fn push_child(&mut self, child: Element) {
593 self.children.push(Content::element(&child));
594 }
595
596 pub fn push_children(&mut self, children: &[Element]) {
598 for child in children {
599 self.children.push(Content::element(child));
600 }
601 }
602
603 pub fn clear_children(&mut self) {
605 self.children.clear();
606 }
607
608 pub fn push_html(&mut self, html: &str) {
613 self.children.push(Content::html(html));
614 }
615
616 pub fn serialize(&self) -> String {
618 format!("{self}")
619 }
620
621 pub fn plain_text(&self) -> String {
624 let mut text = TextVisitor::default();
625 text.visit(self);
626 text.text
627 }
628}
629
630impl Display for Element {
631 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
632 if self.tag().can_self_close() && self.children.is_empty() {
633 write!(f, "<{}{}/>", self.tag, self.attrs)?;
634 } else {
635 write!(f, "<{}{}>", self.tag, self.attrs)?;
636 for child in &self.children {
637 write!(f, "{child}")?;
638 }
639 write!(f, "</{}>", self.tag)?;
640 }
641 Ok(())
642 }
643}
644
645#[cfg(test)]
646mod test_element {
647 use super::{Element, Tag};
648
649 #[test]
650 fn empty_p() {
651 let e = Element::new(Tag::P);
652 assert_eq!(e.to_string(), "<P></P>");
653 }
654
655 #[test]
656 fn empty_br() {
657 let e = Element::new(Tag::Br);
658 assert_eq!(e.to_string(), "<BR/>");
659 }
660}
661
662#[derive(Clone, Debug, Eq, PartialEq)]
664pub enum Content {
665 Text(String),
667 Element(Element),
669 Html(String),
671}
672
673impl Content {
674 pub fn text(s: &str) -> Self {
676 Self::Text(s.into())
677 }
678
679 pub fn element(e: &Element) -> Self {
681 Self::Element(e.clone())
682 }
683
684 pub fn html(s: &str) -> Self {
686 Self::Html(s.into())
687 }
688}
689
690impl Display for Content {
691 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
692 match self {
693 Self::Text(s) => write!(f, "{}", encode_safe(s))?,
694 Self::Element(e) => write!(f, "{e}")?,
695 Self::Html(s) => write!(f, "{s}")?,
696 }
697 Ok(())
698 }
699}
700
701pub trait Visitor {
738 fn visit_element(&mut self, _: &Element) {}
740 fn visit_text(&mut self, _: &str) {}
742 fn visit_html(&mut self, _: &str) {}
744
745 fn visit(&mut self, root: &Element) {
747 self.visit_element(root);
748 for child in &root.children {
749 match child {
750 Content::Text(s) => self.visit_text(s),
751 Content::Element(e) => self.visit(e),
752 Content::Html(s) => self.visit_html(s),
753 }
754 }
755 }
756}
757
758#[derive(Debug, Default)]
772pub struct TextVisitor {
773 pub text: String,
775}
776
777impl Visitor for TextVisitor {
778 fn visit_text(&mut self, s: &str) {
779 self.text.push_str(s);
780 }
781}
782
783#[cfg(test)]
784mod test {
785 use super::{AttributeValue, Content, Element, Tag, Visitor};
786
787 #[test]
788 fn element_has_correct_tag() {
789 let e = Element::new(Tag::P);
790 assert_eq!(e.tag(), Tag::P);
791 }
792
793 #[test]
794 fn element_has_no_attributes_initially() {
795 let e = Element::new(Tag::P);
796 assert_eq!(e.attributes().count(), 0);
797 }
798
799 #[test]
800 fn element_returns_no_value_for_missing_attribute() {
801 let e = Element::new(Tag::P);
802 assert_eq!(e.attribute("foo"), None);
803 }
804
805 #[test]
806 fn can_add_attribute_to_element() {
807 let mut e = Element::new(Tag::P);
808 e.set_attribute("foo", "bar");
809 assert_eq!(
810 e.attribute("foo"),
811 Some(&AttributeValue::String("bar".into()))
812 );
813 assert_eq!(e.attribute("foo").map(|x| x.as_str()), Some("bar"));
814 assert_eq!(e.attribute_value("foo"), Some("bar"));
815 }
816
817 #[test]
818 fn can_create_element_with_attribute() {
819 let e = Element::new(Tag::P).with_attribute("foo", "bar");
820 assert_eq!(
821 e.attribute("foo"),
822 Some(&AttributeValue::String("bar".into()))
823 );
824 }
825
826 #[test]
827 fn can_add_class_to_element() {
828 let mut e = Element::new(Tag::P);
829 e.add_class("foo");
830 let classes: Vec<&str> = e.classes().collect();
831 assert_eq!(classes, ["foo"]);
832 assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
833 }
834
835 #[test]
836 fn can_two_classes_to_element() {
837 let mut e = Element::new(Tag::P);
838 e.add_class("foo");
839 e.add_class("bar");
840 let classes: Vec<&str> = e.classes().collect();
841 assert_eq!(classes, ["foo", "bar"]);
842 assert_eq!(e.to_string(), r#"<P class="foo bar"></P>"#);
843 }
844
845 #[test]
846 fn can_add_same_class_twice_to_element() {
847 let mut e = Element::new(Tag::P);
848 e.add_class("foo");
849 e.add_class("foo");
850 let classes: Vec<&str> = e.classes().collect();
851 assert_eq!(classes, ["foo"]);
852 assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
853 }
854
855 #[test]
856 fn can_add_boolean_attribute_to_element() {
857 let mut e = Element::new(Tag::P);
858 e.set_boolean_attribute("foo");
859 assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
860 }
861
862 #[test]
863 fn can_create_element_with_boolan_attribute() {
864 let e = Element::new(Tag::P).with_boolean_attribute("foo");
865 assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
866 }
867
868 #[test]
869 fn unset_attribute_is_unset() {
870 let e = Element::new(Tag::P);
871 assert_eq!(e.attribute("foo"), None);
872 }
873
874 #[test]
875 fn can_unset_attribute_in_element() {
876 let mut e = Element::new(Tag::P);
877 e.set_attribute("foo", "bar");
878 e.unset_attribute("foo");
879 assert_eq!(e.attribute("foo"), None);
880 }
881
882 #[test]
883 fn element_has_no_children_initially() {
884 let e = Element::new(Tag::P);
885 assert!(e.children.is_empty());
886 }
887
888 #[test]
889 fn add_child_to_element() {
890 let mut e = Element::new(Tag::P);
891 let child = Content::text("foo");
892 e.push_text("foo");
893 assert_eq!(e.children(), &[child]);
894 }
895
896 #[test]
897 fn element_has_no_location_initially() {
898 let e = Element::new(Tag::P);
899 assert!(e.location().is_none());
900 }
901
902 #[test]
903 fn element_with_location() {
904 let e = Element::new(Tag::P).with_location(1, 2);
905 assert_eq!(e.location(), Some((1, 2)));
906 }
907
908 #[test]
909 fn attribute_can_be_serialized() {
910 let mut e = Element::new(Tag::P);
911 e.set_attribute("foo", "bar");
912 assert_eq!(e.serialize(), "<P foo=\"bar\"></P>");
913 }
914
915 #[test]
916 fn dangerous_attribute_value_is_esacped() {
917 let mut e = Element::new(Tag::P);
918 e.set_attribute("foo", "<");
919 assert_eq!(e.serialize(), "<P foo=\"<\"></P>");
920 }
921
922 #[test]
923 fn boolean_attribute_can_be_serialized() {
924 let mut e = Element::new(Tag::P);
925 e.set_boolean_attribute("foo");
926 assert_eq!(e.serialize(), "<P foo></P>");
927 }
928
929 #[test]
930 fn element_can_be_serialized() {
931 let mut e = Element::new(Tag::P);
932 e.push_text("hello ");
933 let mut world = Element::new(Tag::B);
934 world.push_text("world");
935 e.push_child(world);
936 assert_eq!(e.serialize(), "<P>hello <B>world</B></P>");
937 }
938
939 #[test]
940 fn dangerous_text_is_escaped() {
941 let mut e = Element::new(Tag::P);
942 e.push_text("hello <world>");
943 assert_eq!(e.serialize(), "<P>hello <world></P>");
944 }
945
946 #[test]
947 fn element_has_no_class_initially() {
948 let e = Element::new(Tag::P);
949 assert_eq!(e.attribute_value("class"), None);
950 assert_eq!(e.classes().next(), None);
951 assert!(!e.has_class("foo"));
952 }
953
954 #[test]
955 fn element_adds_first_class() {
956 let mut e = Element::new(Tag::P);
957 e.add_class("foo");
958 assert_eq!(e.attribute_value("class"), Some("foo"));
959 assert!(e.has_class("foo"));
960 }
961
962 #[test]
963 fn element_adds_second_class() {
964 let mut e = Element::new(Tag::P);
965 e.add_class("foo");
966 e.add_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 #[test]
973 fn creates_classy_element() {
974 let e = Element::new(Tag::P).with_class("foo").with_class("bar");
975 assert_eq!(e.attribute_value("class"), Some("foo bar"));
976 assert!(e.has_class("foo"));
977 assert!(e.has_class("bar"));
978 }
979
980 #[derive(Default)]
981 struct Collector {
982 tags: Vec<Tag>,
983 text: String,
984 }
985
986 impl Visitor for Collector {
987 fn visit_element(&mut self, e: &Element) {
988 self.tags.push(e.tag());
989 }
990
991 fn visit_text(&mut self, s: &str) {
992 self.text.push_str(s);
993 }
994 }
995
996 #[test]
997 fn visits_all_children() {
998 let e = Element::new(Tag::P)
999 .with_text("hello ")
1000 .with_child(Element::new(Tag::B).with_text("world"));
1001
1002 let mut collector = Collector::default();
1003 collector.visit(&e);
1004 assert_eq!(collector.tags, vec![Tag::P, Tag::B]);
1005 assert_eq!(collector.text, "hello world");
1006 }
1007}