#![deny(missing_docs)]
use html_escape::{encode_double_quoted_attribute, encode_safe};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HtmlPage {
head: Element,
body: Element,
}
impl Default for HtmlPage {
fn default() -> Self {
Self {
head: Element::new(Tag::Head),
body: Element::new(Tag::Body),
}
}
}
impl HtmlPage {
pub fn push_to_head(&mut self, e: Element) {
self.head.push_child(e);
}
pub fn push_to_body(&mut self, e: Element) {
self.body.push_child(e);
}
pub fn with_head_element(mut self, e: Element) -> Self {
self.head.push_child(e);
self
}
pub fn with_body_element(mut self, e: Element) -> Self {
self.body.push_child(e);
self
}
pub fn with_body_text(mut self, text: &str) -> Self {
self.body.push_text(text);
self
}
pub fn push_children(&mut self, e: &Element) {
for child in &e.children {
self.body.children.push(child.clone());
}
}
}
impl Display for HtmlPage {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
writeln!(f, "<!DOCTYPE html>")?;
writeln!(f, "<{}>", Tag::Html)?;
writeln!(f, "{}", &self.head)?;
writeln!(f, "{}", &self.body)?;
writeln!(f, "</{}>", Tag::Html)?;
Ok(())
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[allow(missing_docs)] pub enum Tag {
A,
Abbr,
Address,
Area,
Article,
Aside,
Audio,
B,
Base,
Bdi,
Bdo,
Blockquote,
Body,
Br,
Button,
Canvas,
Caption,
Cite,
Code,
Col,
ColGroup,
Data,
DataList,
Dd,
Del,
Details,
Dfn,
Dialog,
Div,
Dl,
Dt,
Em,
Embed,
FieldSet,
FigCaption,
Figure,
Footer,
Form,
H1,
H2,
H3,
H4,
H5,
H6,
Head,
Header,
Hr,
Html,
I,
Iframe,
Img,
Input,
Ins,
Kbd,
Label,
Legend,
Li,
Link,
Main,
Map,
Mark,
Meta,
Meter,
Nav,
NoScript,
Object,
Ol,
OptGroup,
Option,
Output,
P,
Param,
Picture,
Pre,
Progress,
Q,
Rp,
Rt,
Ruby,
S,
Samp,
Script,
Section,
Select,
Small,
Source,
Span,
Strong,
Style,
Sub,
Summary,
Sup,
Svg,
Table,
Tbody,
Td,
Template,
TextArea,
Tfoot,
Th,
Time,
Title,
Tr,
Track,
U,
Ul,
Var,
Video,
Wbr,
}
impl Display for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.as_str())
}
}
impl Tag {
fn as_str(&self) -> &str {
match self {
Self::A => "A",
Self::Abbr => "ABBR",
Self::Address => "ADDRESS",
Self::Area => "AREA",
Self::Article => "ARTICLE",
Self::Aside => "ASIDE",
Self::Audio => "AUDIO",
Self::B => "B",
Self::Base => "BASE",
Self::Bdi => "BDI",
Self::Bdo => "BDO",
Self::Blockquote => "BLOCKQUOTE",
Self::Body => "BODY",
Self::Br => "BR",
Self::Button => "BUTTON",
Self::Canvas => "CANVAS",
Self::Caption => "CAPTION",
Self::Cite => "CITE",
Self::Code => "CODE",
Self::Col => "COL",
Self::ColGroup => "COLGROUP",
Self::Data => "DATA",
Self::DataList => "DATALIST",
Self::Dd => "DD",
Self::Del => "DEL",
Self::Details => "DETAILS",
Self::Dfn => "DFN",
Self::Dialog => "DIALOG",
Self::Div => "DIV",
Self::Dl => "DL",
Self::Dt => "DT",
Self::Em => "EM",
Self::Embed => "EMBED",
Self::FieldSet => "FIELDSET",
Self::FigCaption => "FIGCAPTIO",
Self::Figure => "FIGURE",
Self::Footer => "FOOTER",
Self::Form => "FORM",
Self::H1 => "H1",
Self::H2 => "H2",
Self::H3 => "H3",
Self::H4 => "H4",
Self::H5 => "H5",
Self::H6 => "H6",
Self::Head => "HEAD",
Self::Header => "HEADER",
Self::Hr => "HR",
Self::Html => "HTML",
Self::I => "I",
Self::Iframe => "IFRAME",
Self::Img => "IMG",
Self::Input => "INPUT",
Self::Ins => "INS",
Self::Kbd => "KBD",
Self::Label => "LABEL",
Self::Legend => "LEGEND",
Self::Li => "LI",
Self::Link => "LINK",
Self::Main => "MAIN",
Self::Map => "MAP",
Self::Mark => "MARK",
Self::Meta => "META",
Self::Meter => "METER",
Self::Nav => "NAV",
Self::NoScript => "NOSCRIPT",
Self::Object => "OBJECT",
Self::Ol => "OL",
Self::OptGroup => "OPTGROUP",
Self::Option => "OPTION",
Self::Output => "OUTPUT",
Self::P => "P",
Self::Param => "PARAM",
Self::Picture => "PICTURE",
Self::Pre => "PRE",
Self::Progress => "PROGRESS",
Self::Q => "Q",
Self::Rp => "RP",
Self::Rt => "RT",
Self::Ruby => "RUBY",
Self::S => "S",
Self::Samp => "SAMP",
Self::Script => "SCRIPT",
Self::Section => "SECTION",
Self::Select => "SELECT",
Self::Small => "SMALL",
Self::Source => "SOURCE",
Self::Span => "SPAN",
Self::Strong => "STRONG",
Self::Style => "STYLE",
Self::Sub => "SUB",
Self::Summary => "SUMMARY",
Self::Sup => "SUP",
Self::Svg => "SVG",
Self::Table => "TABLE",
Self::Tbody => "TBODY",
Self::Td => "TD",
Self::Template => "TEMPLATE",
Self::TextArea => "TEXTAREA",
Self::Tfoot => "TFOOT",
Self::Th => "TH",
Self::Time => "TIME",
Self::Title => "TITLE",
Self::Tr => "TR",
Self::Track => "TRACK",
Self::U => "U",
Self::Ul => "UL",
Self::Var => "VAR",
Self::Video => "VIDEO",
Self::Wbr => "WBR",
}
}
fn can_self_close(&self) -> bool {
match self {
Self::Area
| Self::Base
| Self::Br
| Self::Col
| Self::Embed
| Self::Hr
| Self::Img
| Self::Input
| Self::Link
| Self::Meta
| Self::Param
| Self::Source
| Self::Track
| Self::Wbr => true,
_ => false,
}
}
}
#[cfg(test)]
mod test_tag {
use super::Tag;
#[test]
fn can_self_close() {
assert!(Tag::Area.can_self_close());
assert!(Tag::Base.can_self_close());
assert!(Tag::Br.can_self_close());
assert!(Tag::Col.can_self_close());
assert!(Tag::Embed.can_self_close());
assert!(Tag::Hr.can_self_close());
assert!(Tag::Img.can_self_close());
assert!(Tag::Input.can_self_close());
assert!(Tag::Link.can_self_close());
assert!(Tag::Meta.can_self_close());
assert!(Tag::Param.can_self_close());
assert!(Tag::Source.can_self_close());
assert!(Tag::Track.can_self_close());
assert!(Tag::Wbr.can_self_close());
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct Attributes {
attrs: HashMap<String, AttributeValue>,
}
impl Attributes {
fn set(&mut self, name: &str, value: &str) {
self.attrs
.insert(name.into(), AttributeValue::String(value.into()));
}
fn set_boolean(&mut self, name: &str) {
self.attrs.insert(name.into(), AttributeValue::Boolean);
}
fn unset(&mut self, name: &str) {
self.attrs.remove(name);
}
fn get(&self, name: &str) -> Option<&AttributeValue> {
self.attrs.get(name)
}
fn names(&self) -> impl Iterator<Item = &str> {
self.attrs.keys().map(|s| s.as_ref())
}
}
impl Display for Attributes {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
for (name, value) in self.attrs.iter() {
match value {
AttributeValue::Boolean => write!(f, " {}", name)?,
AttributeValue::String(s) => {
write!(f, " {}=\"{}\"", name, encode_double_quoted_attribute(s))?
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AttributeValue {
String(String),
Boolean,
}
impl AttributeValue {
pub fn as_str(&self) -> &str {
match self {
Self::String(s) => s,
Self::Boolean => "",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Element {
loc: Option<(usize, usize)>,
tag: Tag,
attrs: Attributes,
children: Vec<Content>,
}
impl Element {
pub fn new(tag: Tag) -> Self {
Self {
tag,
attrs: Attributes::default(),
children: vec![],
loc: None,
}
}
pub fn with_location(mut self, line: usize, col: usize) -> Self {
self.loc = Some((line, col));
self
}
pub fn with_child(mut self, child: Element) -> Self {
self.children.push(Content::Element(child));
self
}
pub fn with_text(mut self, child: &str) -> Self {
self.children.push(Content::Text(child.into()));
self
}
pub fn with_attribute(mut self, name: &str, value: &str) -> Self {
self.attrs.set(name, value);
self
}
pub fn with_boolean_attribute(mut self, name: &str) -> Self {
self.attrs.set_boolean(name);
self
}
pub fn with_class(mut self, class: &str) -> Self {
self.add_class(class);
self
}
pub fn tag(&self) -> Tag {
self.tag
}
pub fn location(&self) -> Option<(usize, usize)> {
self.loc
}
pub fn attributes(&self) -> impl Iterator<Item = &str> {
self.attrs.names()
}
pub fn attribute(&self, name: &str) -> Option<&AttributeValue> {
self.attrs.get(name)
}
pub fn attribute_value(&self, name: &str) -> Option<&str> {
self.attrs.get(name).map(|v| v.as_str())
}
pub fn set_attribute(&mut self, name: &str, value: &str) {
self.attrs.set(name, value);
}
pub fn set_boolean_attribute(&mut self, name: &str) {
self.attrs.set_boolean(name);
}
pub fn unset_attribute(&mut self, name: &str) {
self.attrs.unset(name);
}
pub fn classes(&self) -> impl Iterator<Item = &str> {
let v = if let Some(v) = self.attribute_value("class") {
v
} else {
""
};
v.split_ascii_whitespace()
}
pub fn has_class(&self, wanted: &str) -> bool {
self.classes().any(|v| v == wanted)
}
pub fn add_class(&mut self, class: &str) {
if let Some(old) = self.attribute_value("class") {
if !old.split_ascii_whitespace().any(|s| s == class) {
self.set_attribute("class", &format!("{old} {class}"));
}
} else {
self.set_attribute("class", class);
}
}
pub fn push_text(&mut self, text: &str) {
self.children.push(Content::text(text));
}
pub fn push_child(&mut self, child: Element) {
self.children.push(Content::element(&child));
}
pub fn clear_children(&mut self) {
self.children.clear();
}
pub fn push_html(&mut self, html: &str) {
self.children.push(Content::html(html));
}
pub fn serialize(&self) -> String {
format!("{}", self)
}
pub fn plain_text(&self) -> String {
let mut text = TextVisitor::default();
text.visit(self);
text.text
}
}
impl Display for Element {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
if self.tag().can_self_close() && self.children.is_empty() {
write!(f, "<{}{}/>", self.tag, self.attrs)?;
} else {
write!(f, "<{}{}>", self.tag, self.attrs)?;
for child in &self.children {
write!(f, "{}", child)?;
}
write!(f, "</{}>", self.tag)?;
}
Ok(())
}
}
#[cfg(test)]
mod test_element {
use super::{Element, Tag};
#[test]
fn empty_p() {
let e = Element::new(Tag::P);
assert_eq!(e.to_string(), "<P></P>");
}
#[test]
fn empty_br() {
let e = Element::new(Tag::Br);
assert_eq!(e.to_string(), "<BR/>");
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Content {
Text(String),
Element(Element),
Html(String),
}
impl Content {
pub fn text(s: &str) -> Self {
Self::Text(s.into())
}
pub fn element(e: &Element) -> Self {
Self::Element(e.clone())
}
pub fn html(s: &str) -> Self {
Self::Html(s.into())
}
}
impl Display for Content {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::Text(s) => write!(f, "{}", encode_safe(s))?,
Self::Element(e) => write!(f, "{}", e)?,
Self::Html(s) => write!(f, "{}", s)?,
}
Ok(())
}
}
pub trait Visitor {
fn visit_element(&mut self, _: &Element) {}
fn visit_text(&mut self, _: &str) {}
fn visit_html(&mut self, _: &str) {}
fn visit(&mut self, root: &Element) {
self.visit_element(root);
for child in &root.children {
match child {
Content::Text(s) => self.visit_text(s),
Content::Element(e) => self.visit(e),
Content::Html(s) => self.visit_html(s),
}
}
}
}
#[derive(Debug, Default)]
pub struct TextVisitor {
pub text: String,
}
impl Visitor for TextVisitor {
fn visit_text(&mut self, s: &str) {
self.text.push_str(s);
}
}
#[cfg(test)]
mod test {
use super::{AttributeValue, Content, Element, Tag, Visitor};
#[test]
fn element_has_correct_tag() {
let e = Element::new(Tag::P);
assert_eq!(e.tag(), Tag::P);
}
#[test]
fn element_has_no_attributes_initially() {
let e = Element::new(Tag::P);
assert_eq!(e.attributes().count(), 0);
}
#[test]
fn element_returns_no_value_for_missing_attribute() {
let e = Element::new(Tag::P);
assert_eq!(e.attribute("foo"), None);
}
#[test]
fn can_add_attribute_to_element() {
let mut e = Element::new(Tag::P);
e.set_attribute("foo", "bar");
assert_eq!(
e.attribute("foo"),
Some(&AttributeValue::String("bar".into()))
);
assert_eq!(e.attribute("foo").map(|x| x.as_str()), Some("bar"));
assert_eq!(e.attribute_value("foo"), Some("bar"));
}
#[test]
fn can_create_element_with_attribute() {
let e = Element::new(Tag::P).with_attribute("foo", "bar");
assert_eq!(
e.attribute("foo"),
Some(&AttributeValue::String("bar".into()))
);
}
#[test]
fn can_add_class_to_element() {
let mut e = Element::new(Tag::P);
e.add_class("foo");
let classes: Vec<&str> = e.classes().collect();
assert_eq!(classes, ["foo"]);
assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
}
#[test]
fn can_two_classes_to_element() {
let mut e = Element::new(Tag::P);
e.add_class("foo");
e.add_class("bar");
let classes: Vec<&str> = e.classes().collect();
assert_eq!(classes, ["foo", "bar"]);
assert_eq!(e.to_string(), r#"<P class="foo bar"></P>"#);
}
#[test]
fn can_add_same_class_twice_to_element() {
let mut e = Element::new(Tag::P);
e.add_class("foo");
e.add_class("foo");
let classes: Vec<&str> = e.classes().collect();
assert_eq!(classes, ["foo"]);
assert_eq!(e.to_string(), r#"<P class="foo"></P>"#);
}
#[test]
fn can_add_boolean_attribute_to_element() {
let mut e = Element::new(Tag::P);
e.set_boolean_attribute("foo");
assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
}
#[test]
fn can_create_element_with_boolan_attribute() {
let e = Element::new(Tag::P).with_boolean_attribute("foo");
assert_eq!(e.attribute("foo"), Some(&AttributeValue::Boolean));
}
#[test]
fn unset_attribute_is_unset() {
let e = Element::new(Tag::P);
assert_eq!(e.attribute("foo"), None);
}
#[test]
fn can_unset_attribute_in_element() {
let mut e = Element::new(Tag::P);
e.set_attribute("foo", "bar");
e.unset_attribute("foo");
assert_eq!(e.attribute("foo"), None);
}
#[test]
fn element_has_no_children_initially() {
let e = Element::new(Tag::P);
assert!(e.children.is_empty());
}
#[test]
fn add_child_to_element() {
let mut e = Element::new(Tag::P);
let child = Content::text("foo");
e.push_text("foo");
assert_eq!(e.children, &[child]);
}
#[test]
fn element_has_no_location_initially() {
let e = Element::new(Tag::P);
assert!(e.location().is_none());
}
#[test]
fn element_with_location() {
let e = Element::new(Tag::P).with_location(1, 2);
assert_eq!(e.location(), Some((1, 2)));
}
#[test]
fn attribute_can_be_serialized() {
let mut e = Element::new(Tag::P);
e.set_attribute("foo", "bar");
assert_eq!(e.serialize(), "<P foo=\"bar\"></P>");
}
#[test]
fn dangerous_attribute_value_is_esacped() {
let mut e = Element::new(Tag::P);
e.set_attribute("foo", "<");
assert_eq!(e.serialize(), "<P foo=\"<\"></P>");
}
#[test]
fn boolean_attribute_can_be_serialized() {
let mut e = Element::new(Tag::P);
e.set_boolean_attribute("foo");
assert_eq!(e.serialize(), "<P foo></P>");
}
#[test]
fn element_can_be_serialized() {
let mut e = Element::new(Tag::P);
e.push_text("hello ");
let mut world = Element::new(Tag::B);
world.push_text("world");
e.push_child(world);
assert_eq!(e.serialize(), "<P>hello <B>world</B></P>");
}
#[test]
fn dangerous_text_is_escaped() {
let mut e = Element::new(Tag::P);
e.push_text("hello <world>");
assert_eq!(e.serialize(), "<P>hello <world></P>");
}
#[test]
fn element_has_no_class_initially() {
let e = Element::new(Tag::P);
assert_eq!(e.attribute_value("class"), None);
assert_eq!(e.classes().next(), None);
assert!(!e.has_class("foo"));
}
#[test]
fn element_adds_first_class() {
let mut e = Element::new(Tag::P);
e.add_class("foo");
assert_eq!(e.attribute_value("class"), Some("foo"));
assert!(e.has_class("foo"));
}
#[test]
fn element_adds_second_class() {
let mut e = Element::new(Tag::P);
e.add_class("foo");
e.add_class("bar");
assert_eq!(e.attribute_value("class"), Some("foo bar"));
assert!(e.has_class("foo"));
assert!(e.has_class("bar"));
}
#[test]
fn creates_classy_element() {
let e = Element::new(Tag::P).with_class("foo").with_class("bar");
assert_eq!(e.attribute_value("class"), Some("foo bar"));
assert!(e.has_class("foo"));
assert!(e.has_class("bar"));
}
#[derive(Default)]
struct Collector {
tags: Vec<Tag>,
text: String,
}
impl Visitor for Collector {
fn visit_element(&mut self, e: &Element) {
self.tags.push(e.tag());
}
fn visit_text(&mut self, s: &str) {
self.text.push_str(s);
}
}
#[test]
fn visits_all_children() {
let e = Element::new(Tag::P)
.with_text("hello ")
.with_child(Element::new(Tag::B).with_text("world"));
let mut collector = Collector::default();
collector.visit(&e);
assert_eq!(collector.tags, vec![Tag::P, Tag::B]);
assert_eq!(collector.text, "hello world");
}
}