Skip to content

Commit

Permalink
add <meta> tag
Browse files Browse the repository at this point in the history
  • Loading branch information
jcornaz committed Nov 1, 2024
1 parent 4aba441 commit 946aae0
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 78 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
88 changes: 58 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Attribute>,
children: Vec<Element>,
},
Void {
tag: &'static str,
attributes: Vec<Attribute>,
children: Vec<Node>,
},
Text(Cow<'static, str>),
Raw(Cow<'static, str>),
Multiple(Vec<Node>),
Multiple(Vec<Element>),
}

#[derive(Debug, Clone)]
Expand All @@ -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", [], [])],
))
}
}
Expand All @@ -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<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
children: impl IntoIterator<Item = Element>,
) -> 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<Item = Attribute>) -> Self {
Self(ElementInner::Void {
tag,
attributes: attributes.into_iter().collect(),
})
}
}

impl From<NodeInner> for Node {
fn from(value: NodeInner) -> Self {
impl From<ElementInner> 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, "</{tag}>")?;
}
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}")?;
}
Expand All @@ -111,6 +122,23 @@ impl Display for Node {
}
}

fn write_attributes(
f: &mut std::fmt::Formatter<'_>,
attributes: &Vec<Attribute>,
) -> 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<Cow<'static, str>>) -> Self {
debug_assert!(
Expand All @@ -126,7 +154,7 @@ impl Attribute {

pub fn html(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
children: impl IntoIterator<Item = Element>,
) -> Document {
Document(Node::new("html", attributes, children))
Document(Element::new("html", attributes, children))
}
92 changes: 48 additions & 44 deletions src/nodes.rs
Original file line number Diff line number Diff line change
@@ -1,89 +1,93 @@
use std::borrow::Cow;

use crate::{Attribute, Node, NodeInner};
use crate::{Attribute, Element, ElementInner};

pub fn div(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("div", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("div", attributes, children)
}

pub fn head(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("head", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("head", attributes, children)
}

pub fn meta(attributes: impl IntoIterator<Item = Attribute>) -> Element {
Element::new_void("meta", attributes)
}

pub fn title(
attributes: impl IntoIterator<Item = Attribute>,
text: impl Into<Cow<'static, str>>,
) -> Node {
Node::new("title", attributes, [text.into().into()])
) -> Element {
Element::new("title", attributes, [text.into().into()])
}

pub fn body(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("body", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("body", attributes, children)
}

pub fn h1(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("h1", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("h1", attributes, children)
}

pub fn h2(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("h2", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("h2", attributes, children)
}

pub fn h3(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("h3", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("h3", attributes, children)
}

pub fn h4(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("h4", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("h4", attributes, children)
}

pub fn h5(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("h5", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("h5", attributes, children)
}

pub fn h6(
attributes: impl IntoIterator<Item = Attribute>,
children: impl IntoIterator<Item = Node>,
) -> Node {
Node::new("h6", attributes, children)
children: impl IntoIterator<Item = Element>,
) -> Element {
Element::new("h6", attributes, children)
}

/// HTML escaped text
pub fn text(value: impl Into<Cow<'static, str>>) -> Node {
NodeInner::Text(value.into()).into()
pub fn text(value: impl Into<Cow<'static, str>>) -> Element {
ElementInner::Text(value.into()).into()
}

/// Inline raw HTML without escaping
///
/// 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
Expand All @@ -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<Cow<'static, str>> for Node {
impl From<Cow<'static, str>> 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<String> for Node {
impl From<String> for Element {
fn from(value: String) -> Self {
text(value)
}
}

impl<const N: usize> From<[Node; N]> for Node {
fn from(value: [Node; N]) -> Self {
impl<const N: usize> From<[Element; N]> for Element {
fn from(value: [Element; N]) -> Self {
Vec::from(value).into()
}
}

impl From<Vec<Node>> for Node {
fn from(value: Vec<Node>) -> Self {
Self(NodeInner::Multiple(value))
impl From<Vec<Element>> for Element {
fn from(value: Vec<Element>) -> Self {
Self(ElementInner::Multiple(value))
}
}
7 changes: 4 additions & 3 deletions tests/render_spec.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -44,6 +44,7 @@ fn should_render_attribute(#[case] attr: Attribute, #[case] expected: &str) {
#[case([div([], ["a".into()]), div([], ["b".into()])].into(), "<div>a</div><div>b</div>")]
#[case(raw("<my-component></my-component>"), "<my-component></my-component>")]
#[case(raw_unsafe("<my-component></my-component>".to_string()), "<my-component></my-component>")]
#[case(meta([("foo", "bar").into()]), "<meta foo=\"bar\">")]
#[case(div([("foo", "bar").into()], ["hello".into()]), "<div foo=\"bar\">hello</div>")]
#[case(div([("foo", "bar".to_string()).into()], [text("hello".to_string())]), "<div foo=\"bar\">hello</div>")]
#[case(head([id("foo")], [text("hello")]), "<head id=\"foo\">hello</head>")]
Expand All @@ -55,7 +56,7 @@ fn should_render_attribute(#[case] attr: Attribute, #[case] expected: &str) {
#[case(h4([id("foo")], [text("hello")]), "<h4 id=\"foo\">hello</h4>")]
#[case(h5([id("foo")], [text("hello")]), "<h5 id=\"foo\">hello</h5>")]
#[case(h6([id("foo")], [text("hello")]), "<h6 id=\"foo\">hello</h6>")]
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);
}

Expand Down Expand Up @@ -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, [], []);
}

0 comments on commit 946aae0

Please sign in to comment.