diff --git a/CHANGELOG.md b/CHANGELOG.md index 41fa111..609a36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * `lang`, `id` and `class` attributes * Nodes * Html document (`html`, `head` and `body`) - * Meta (`title`) + * Meta (`meta`, `title`) * Text (`h1` to `h6`, and `text`) * Container (`div`) * Escape hatches (`raw` and `raw_unsafe`) diff --git a/src/lib.rs b/src/lib.rs index b520fc2..134c0c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,21 +6,25 @@ pub mod nodes; use std::{borrow::Cow, fmt::Display}; #[derive(Debug, Clone)] -pub struct Document(Node); +pub struct Document(Element); #[derive(Debug, Clone)] -pub struct Node(NodeInner); +pub struct Element(ElementInner); #[derive(Debug, Clone)] -enum NodeInner { - Node { +enum ElementInner { + Parent { + tag: &'static str, + attributes: Vec, + children: Vec, + }, + Void { tag: &'static str, attributes: Vec, - children: Vec, }, Text(Cow<'static, str>), Raw(Cow<'static, str>), - Multiple(Vec), + Multiple(Vec), } #[derive(Debug, Clone)] @@ -36,10 +40,10 @@ enum AttributeValue { impl Default for Document { fn default() -> Self { - Self(Node::new( + Self(Element::new( "html", [], - [Node::new("head", [], []), Node::new("body", [], [])], + [Element::new("head", [], []), Element::new("body", [], [])], )) } } @@ -50,58 +54,65 @@ impl Display for Document { } } -impl Node { +impl Element { + /// Create a new HTML element from its tag, attributes, and children pub fn new( tag: &'static str, attributes: impl IntoIterator, - children: impl IntoIterator, + children: impl IntoIterator, ) -> Self { debug_assert!( !tag.is_empty() && tag.chars().all(|c| !c.is_whitespace()), "invalid attribute name: '{tag}'" ); - Self(NodeInner::Node { + Self(ElementInner::Parent { tag, attributes: attributes.into_iter().map(Into::into).collect(), children: children.into_iter().collect(), }) } + + /// Create a new [void] HTML element from its tag and attributes + /// + /// [void]: https://developer.mozilla.org/en-US/docs/Glossary/Void_element + pub fn new_void(tag: &'static str, attributes: impl IntoIterator) -> Self { + Self(ElementInner::Void { + tag, + attributes: attributes.into_iter().collect(), + }) + } } -impl From for Node { - fn from(value: NodeInner) -> Self { +impl From for Element { + fn from(value: ElementInner) -> Self { Self(value) } } -impl Display for Node { +impl Display for Element { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.0 { - NodeInner::Node { + ElementInner::Parent { tag, attributes, children, } => { write!(f, "<{tag}")?; - for attribute in attributes { - match &attribute.value { - AttributeValue::String(s) => write!( - f, - " {}=\"{}\"", - attribute.name, - html_escape::encode_double_quoted_attribute(s) - )?, - } - } + write_attributes(f, attributes)?; write!(f, ">")?; for child in children { write!(f, "{child}")?; } write!(f, "")?; } - NodeInner::Text(text) => write!(f, "{}", html_escape::encode_text(text))?, - NodeInner::Raw(raw) => write!(f, "{raw}")?, - NodeInner::Multiple(nodes) => { + ElementInner::Void { tag, attributes } => { + write!(f, "<{tag}")?; + write_attributes(f, attributes)?; + write!(f, ">")?; + } + ElementInner::Text(text) => write!(f, "{}", html_escape::encode_text(text))?, + ElementInner::Raw(raw) => write!(f, "{raw}")?, + ElementInner::Multiple(nodes) => { for node in nodes { write!(f, "{node}")?; } @@ -111,6 +122,23 @@ impl Display for Node { } } +fn write_attributes( + f: &mut std::fmt::Formatter<'_>, + attributes: &Vec, +) -> Result<(), std::fmt::Error> { + for attribute in attributes { + match &attribute.value { + AttributeValue::String(s) => write!( + f, + " {}=\"{}\"", + attribute.name, + html_escape::encode_double_quoted_attribute(s) + )?, + } + } + Ok(()) +} + impl Attribute { pub fn new(name: &'static str, value: impl Into>) -> Self { debug_assert!( @@ -126,7 +154,7 @@ impl Attribute { pub fn html( attributes: impl IntoIterator, - children: impl IntoIterator, + children: impl IntoIterator, ) -> Document { - Document(Node::new("html", attributes, children)) + Document(Element::new("html", attributes, children)) } diff --git a/src/nodes.rs b/src/nodes.rs index 3880aaa..780a3ef 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -1,80 +1,84 @@ use std::borrow::Cow; -use crate::{Attribute, Node, NodeInner}; +use crate::{Attribute, Element, ElementInner}; pub fn div( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("div", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("div", attributes, children) } pub fn head( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("head", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("head", attributes, children) +} + +pub fn meta(attributes: impl IntoIterator) -> Element { + Element::new_void("meta", attributes) } pub fn title( attributes: impl IntoIterator, text: impl Into>, -) -> Node { - Node::new("title", attributes, [text.into().into()]) +) -> Element { + Element::new("title", attributes, [text.into().into()]) } pub fn body( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("body", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("body", attributes, children) } pub fn h1( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("h1", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("h1", attributes, children) } pub fn h2( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("h2", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("h2", attributes, children) } pub fn h3( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("h3", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("h3", attributes, children) } pub fn h4( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("h4", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("h4", attributes, children) } pub fn h5( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("h5", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("h5", attributes, children) } pub fn h6( attributes: impl IntoIterator, - children: impl IntoIterator, -) -> Node { - Node::new("h6", attributes, children) + children: impl IntoIterator, +) -> Element { + Element::new("h6", attributes, children) } /// HTML escaped text -pub fn text(value: impl Into>) -> Node { - NodeInner::Text(value.into()).into() +pub fn text(value: impl Into>) -> Element { + ElementInner::Text(value.into()).into() } /// Inline raw HTML without escaping @@ -82,8 +86,8 @@ pub fn text(value: impl Into>) -> Node { /// This function is considered safe because the HTML being inlined must be known at compile time /// /// See [`raw_unsafe`] to inline HTML that is generated at runtime -pub fn raw(html: &'static str) -> Node { - NodeInner::Raw(html.into()).into() +pub fn raw(html: &'static str) -> Element { + ElementInner::Raw(html.into()).into() } /// Inline raw HTML without escaping @@ -92,36 +96,36 @@ pub fn raw(html: &'static str) -> Node { /// Miss-use can lead to XSS vulnerability. /// /// See [`raw`] to safely inline HTML that is known at compile time -pub fn raw_unsafe(html: String) -> Node { - NodeInner::Raw(html.into()).into() +pub fn raw_unsafe(html: String) -> Element { + ElementInner::Raw(html.into()).into() } -impl From> for Node { +impl From> for Element { fn from(value: Cow<'static, str>) -> Self { text(value) } } -impl From<&'static str> for Node { +impl From<&'static str> for Element { fn from(value: &'static str) -> Self { text(value) } } -impl From for Node { +impl From for Element { fn from(value: String) -> Self { text(value) } } -impl From<[Node; N]> for Node { - fn from(value: [Node; N]) -> Self { +impl From<[Element; N]> for Element { + fn from(value: [Element; N]) -> Self { Vec::from(value).into() } } -impl From> for Node { - fn from(value: Vec) -> Self { - Self(NodeInner::Multiple(value)) +impl From> for Element { + fn from(value: Vec) -> Self { + Self(ElementInner::Multiple(value)) } } diff --git a/tests/render_spec.rs b/tests/render_spec.rs index e3ae35f..f119b83 100644 --- a/tests/render_spec.rs +++ b/tests/render_spec.rs @@ -1,6 +1,6 @@ use rstest::rstest; -use fun_html::{attributes::*, html, nodes::*, Attribute, Document, Node}; +use fun_html::{attributes::*, html, nodes::*, Attribute, Document, Element}; #[test] fn should_render_empty_document() { @@ -44,6 +44,7 @@ fn should_render_attribute(#[case] attr: Attribute, #[case] expected: &str) { #[case([div([], ["a".into()]), div([], ["b".into()])].into(), "
a
b
")] #[case(raw(""), "")] #[case(raw_unsafe("".to_string()), "")] +#[case(meta([("foo", "bar").into()]), "")] #[case(div([("foo", "bar").into()], ["hello".into()]), "
hello
")] #[case(div([("foo", "bar".to_string()).into()], [text("hello".to_string())]), "
hello
")] #[case(head([id("foo")], [text("hello")]), "hello")] @@ -55,7 +56,7 @@ fn should_render_attribute(#[case] attr: Attribute, #[case] expected: &str) { #[case(h4([id("foo")], [text("hello")]), "

hello

")] #[case(h5([id("foo")], [text("hello")]), "
hello
")] #[case(h6([id("foo")], [text("hello")]), "
hello
")] -fn should_render_node(#[case] def: Node, #[case] expected: &str) { +fn should_render_node(#[case] def: Element, #[case] expected: &str) { assert_eq!(def.to_string(), expected); } @@ -94,5 +95,5 @@ fn should_panic_for_invalid_attribute_name( fn should_panic_for_invalid_tag_name( #[values("hello world", "hello\tworld", "hello\nworld", "")] name: &'static str, ) { - Node::new(name, [], []); + Element::new(name, [], []); }