use crate::html_escaping::escape_html;
use crate::Render;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::{Result, Write};
#[derive(Debug)]
pub enum AttrValue<'a> {
Value(Cow<'a, str>),
Boolean,
}
type Attributes<'a> = Option<HashMap<&'a str, AttrValue<'a>>>;
#[derive(Debug)]
pub struct SimpleElement<'a, T: Render> {
pub tag_name: &'a str,
pub attributes: Attributes<'a>,
pub contents: Option<T>,
}
const VOID_ELEMENTS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
];
fn is_void_element(tag: &str) -> bool {
VOID_ELEMENTS.contains(&tag)
}
fn write_attributes<'a, W: Write>(maybe_attributes: Attributes<'a>, writer: &mut W) -> Result {
match maybe_attributes {
None => Ok(()),
Some(mut attributes) => {
for (key, value) in attributes.drain() {
match value {
AttrValue::Boolean => {
write!(writer, " {}", key)?;
}
AttrValue::Value(v) => {
write!(writer, " {}=\"", key)?;
escape_html(&v, writer)?;
write!(writer, "\"")?;
}
}
}
Ok(())
}
}
}
impl<T: Render> Render for SimpleElement<'_, T> {
fn render_into<W: Write>(self, writer: &mut W) -> Result {
let is_void = is_void_element(self.tag_name);
match self.contents {
None => {
write!(writer, "<{}", self.tag_name)?;
write_attributes(self.attributes, writer)?;
if is_void {
write!(writer, ">")
} else {
write!(writer, "></{}>", self.tag_name)
}
}
Some(renderable) => {
write!(writer, "<{}", self.tag_name)?;
write_attributes(self.attributes, writer)?;
write!(writer, ">")?;
renderable.render_into(writer)?;
if !is_void {
write!(writer, "</{}>", self.tag_name)?;
}
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
#[test]
fn script_empty_renders_open_and_close_tag() {
let el: SimpleElement<'_, ()> = SimpleElement {
tag_name: "script",
attributes: {
let mut attrs = HashMap::new();
attrs.insert("src", AttrValue::Value(Cow::Borrowed("app.js")));
Some(attrs)
},
contents: None,
};
let result = el.render();
assert!(
result.contains("></script>"),
"Expected closing tag, got: {}",
result
);
assert!(
!result.contains("/>"),
"Should not self-close script, got: {}",
result
);
}
#[test]
fn script_with_content_renders_open_and_close_tag() {
let el = SimpleElement {
tag_name: "script",
attributes: None,
contents: Some("console.log(1)"),
};
let result = el.render();
assert_eq!(result, "<script>console.log(1)</script>");
}
#[test]
fn div_empty_renders_open_and_close_tag() {
let el: SimpleElement<'_, ()> = SimpleElement {
tag_name: "div",
attributes: None,
contents: None,
};
assert_eq!(el.render(), "<div></div>");
}
#[test]
fn void_element_no_closing_tag() {
let el: SimpleElement<'_, ()> = SimpleElement {
tag_name: "br",
attributes: None,
contents: None,
};
assert_eq!(el.render(), "<br>");
}
#[test]
fn void_element_input_no_closing_tag() {
let el: SimpleElement<'_, ()> = SimpleElement {
tag_name: "input",
attributes: {
let mut attrs = HashMap::new();
attrs.insert("type", AttrValue::Value(Cow::Borrowed("text")));
Some(attrs)
},
contents: None,
};
let result = el.render();
assert!(result.starts_with("<input"));
assert!(result.ends_with(">"));
assert!(!result.contains("</input>"));
}
}