use std::fmt::Write;
use askama::filters::Escaper;
use derive_more::{Deref, Display, From};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deref, From, Display)]
pub struct Html(pub String);
impl Html {
#[must_use]
pub fn new<T: Into<String>>(html: T) -> Self {
Self(html.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
impl AsRef<str> for Html {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug)]
pub struct HtmlTag {
tag: String,
attributes: Vec<(String, String)>,
boolean_attributes: Vec<String>,
}
impl HtmlTag {
#[must_use]
pub fn new(tag: &str) -> Self {
Self {
tag: tag.to_string(),
attributes: Vec::new(),
boolean_attributes: Vec::new(),
}
}
#[must_use]
pub fn input(input_type: &str) -> Self {
let mut input = Self::new("input");
input.attr("type", input_type);
input
}
pub fn attr(&mut self, key: &str, value: &str) -> &mut Self {
assert!(
!self.attributes.iter().any(|(k, _)| k == key),
"Attribute already exists: {key}"
);
self.attributes.push((key.to_string(), value.to_string()));
self
}
pub fn bool_attr(&mut self, key: &str) -> &mut Self {
assert!(
!self.boolean_attributes.contains(&key.to_string()),
"Boolean attribute already exists: {key}"
);
self.boolean_attributes.push(key.to_string());
self
}
#[must_use]
pub fn render(&self) -> Html {
const FAIL_MSG: &str = "Failed to write HTML tag";
let mut result = String::new();
write!(&mut result, "<{}", self.tag).expect(FAIL_MSG);
for (key, value) in &self.attributes {
write!(&mut result, " {key}=\"").expect(FAIL_MSG);
askama::filters::Html
.write_escaped_str(&mut result, value)
.expect(FAIL_MSG);
write!(&mut result, "\"").expect(FAIL_MSG);
}
for key in &self.boolean_attributes {
write!(&mut result, " {key}").expect(FAIL_MSG);
}
write!(&mut result, " />").expect(FAIL_MSG);
result.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_html_new() {
let html = Html::new("<div>Hello</div>");
assert_eq!(html.as_str(), "<div>Hello</div>");
}
#[test]
fn test_html_tag_new() {
let tag = HtmlTag::new("div");
assert_eq!(tag.render().as_str(), "<div />");
}
#[test]
fn test_html_tag_with_attributes() {
let mut tag = HtmlTag::new("input");
tag.attr("type", "text").attr("placeholder", "Enter text");
assert_eq!(
tag.render().as_str(),
"<input type=\"text\" placeholder=\"Enter text\" />"
);
}
#[test]
fn test_html_tag_escaping() {
let mut tag = HtmlTag::new("input");
tag.attr("type", "text").attr("placeholder", "<>&\"'");
assert_eq!(
tag.render().as_str(),
"<input type=\"text\" placeholder=\"<>&"'\" />"
);
}
#[test]
fn test_html_tag_with_boolean_attributes() {
let mut tag = HtmlTag::new("input");
tag.bool_attr("disabled");
assert_eq!(tag.render().as_str(), "<input disabled />");
}
#[test]
fn test_html_tag_input() {
let mut input = HtmlTag::input("text");
input.attr("name", "username");
assert_eq!(
input.render().as_str(),
"<input type=\"text\" name=\"username\" />"
);
}
}