use std::fmt::Write as _;
use autumn_web::{Markup, PreEscaped};
use maud::Escaper;
pub type Attr = (&'static str, String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Tag {
#[default]
Div,
Section,
Article,
Header,
Footer,
Aside,
Nav,
Main,
Figure,
Blockquote,
Ul,
Ol,
Li,
P,
Span,
H1,
H2,
H3,
H4,
H5,
H6,
}
impl Tag {
pub const ALL: &'static [Self] = &[
Self::Div,
Self::Section,
Self::Article,
Self::Header,
Self::Footer,
Self::Aside,
Self::Nav,
Self::Main,
Self::Figure,
Self::Blockquote,
Self::Ul,
Self::Ol,
Self::Li,
Self::P,
Self::Span,
Self::H1,
Self::H2,
Self::H3,
Self::H4,
Self::H5,
Self::H6,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Div => "div",
Self::Section => "section",
Self::Article => "article",
Self::Header => "header",
Self::Footer => "footer",
Self::Aside => "aside",
Self::Nav => "nav",
Self::Main => "main",
Self::Figure => "figure",
Self::Blockquote => "blockquote",
Self::Ul => "ul",
Self::Ol => "ol",
Self::Li => "li",
Self::P => "p",
Self::Span => "span",
Self::H1 => "h1",
Self::H2 => "h2",
Self::H3 => "h3",
Self::H4 => "h4",
Self::H5 => "h5",
Self::H6 => "h6",
}
}
}
#[cfg(test)]
pub(crate) fn wrap_div(attrs: &[Attr], inner: &Markup) -> Markup {
wrap_el(Tag::Div, attrs, inner)
}
pub(crate) fn wrap_el(tag: Tag, attrs: &[Attr], inner: &Markup) -> Markup {
let mut html = format!("<{}", tag.name());
for (name, value) in attrs {
html.push(' ');
html.push_str(name);
if !value.is_empty() {
html.push_str("=\"");
let _ = Escaper::new(&mut html).write_str(value);
html.push('"');
}
}
html.push('>');
html.push_str(&inner.0);
html.push_str("</");
html.push_str(tag.name());
html.push('>');
PreEscaped(html)
}
pub(crate) fn push_flag(out: &mut Vec<Attr>, name: &'static str, on: bool) {
if on {
out.push((name, String::new()));
}
}
pub(crate) fn push_opt(out: &mut Vec<Attr>, name: &'static str, value: Option<String>) {
if let Some(value) = value {
out.push((name, value));
}
}
#[cfg(test)]
mod tests {
use super::*;
use autumn_web::html;
#[test]
fn wrap_div_writes_attributes_in_order() {
let attrs = vec![
("data-gsap", "fade-up".to_owned()),
("data-gsap-pin", String::new()),
("data-gsap-delay", "0.2".to_owned()),
];
let html = wrap_div(&attrs, &html! { p { "hi" } }).into_string();
assert_eq!(
html,
r#"<div data-gsap="fade-up" data-gsap-pin data-gsap-delay="0.2"><p>hi</p></div>"#
);
}
#[test]
fn wrap_div_escapes_values() {
let attrs = vec![("data-gsap-trigger", r#"a[href="x"]&<b>"#.to_owned())];
let html = wrap_div(&attrs, &html! {}).into_string();
assert_eq!(
html,
r#"<div data-gsap-trigger="a[href="x"]&<b>"></div>"#
);
}
#[test]
fn wrap_el_uses_the_tag_name() {
let attrs = vec![
("class", "card".to_owned()),
("data-gsap", "fade".to_owned()),
];
let html = wrap_el(Tag::H2, &attrs, &html! { "Title" }).into_string();
assert_eq!(html, r#"<h2 class="card" data-gsap="fade">Title</h2>"#);
}
#[test]
fn tag_names_are_lowercase_html() {
for tag in Tag::ALL {
let name = tag.name();
assert!(
name.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()),
"{name}"
);
}
assert_eq!(Tag::default(), Tag::Div);
assert_eq!(Tag::Blockquote.name(), "blockquote");
}
#[test]
fn push_helpers_skip_absent_values() {
let mut out = Vec::new();
push_flag(&mut out, "data-gsap-pin", false);
push_opt(&mut out, "data-gsap-end", None);
assert_eq!(out, Vec::<Attr>::new());
push_flag(&mut out, "data-gsap-pin", true);
push_opt(&mut out, "data-gsap-end", Some("bottom top".to_owned()));
assert_eq!(
out,
vec![
("data-gsap-pin", String::new()),
("data-gsap-end", "bottom top".to_owned()),
]
);
}
}