~eanyanwu/toph

e9978e813b46ec7f252ca035cd214a70bbdbb1ca — Eze 2 months ago 7d8034a
Refactor Node implementation

* Node is no longer and enum, but a struct.
* The Fragment type is gone. The list of child nodes is stored
directly on the parent node. Functions that need to accept multiple
nodes indicate so by their signature. This unlocks the
ability to specify that a method only accepts one node, which wasn't
possible before since you could convert a list of nodes into one.
8 files changed, 293 insertions(+), 447 deletions(-)

D examples/posthaven/main.rs
M src/component.rs
M src/lib.rs
M src/node.rs
D src/node/asset.rs
M src/node/attribute.rs
M src/node/tag.rs
M src/node/visitor.rs
D examples/posthaven/main.rs => examples/posthaven/main.rs +0 -39
@@ 1,39 0,0 @@
use std::error::Error;
use std::fs;
use toph::{component::*, tag::*, Node};

fn button(text: &str) -> Node {
    let css = r#"
        button {
            padding: 0.5rem 1.25rem;
            background-color: #ffffff;
            border-radius: 0.25rem;
        }
    "#;
    button_.set(t_(text)).stylesheet(css)
}
fn header() -> Node {
    let nav_elements = [
        "Features",
        "Screenshots",
        "Our pledge",
        "Pricing",
        "Questions?",
    ];

    let li_items = nav_elements.into_iter().map(|e| li_.set(a_.set(t_(e))));
    let nav = ul_.set(li_items);
    let login = button("Login");
    let cta = button("Get Started");
    header_.set([nav, div_.set([login, cta])])
}
fn main() -> Result<(), Box<dyn Error>> {
    let mut html: Node = [
        doctype_,
        html_.set([head_, body_.set([css_reset(), header()])]),
    ]
    .into();

    fs::write("posthaven.html", html.write_to_string(true))?;
    Ok(())
}

M src/component.rs => src/component.rs +39 -25
@@ 63,7 63,11 @@ impl Display for Ratio {
/// +---+
///   x no gap
/// ```
pub fn stack(gap: impl Into<ModularSpacing>, child: impl Into<Node>) -> Node {
pub fn stack<I, E>(gap: impl Into<ModularSpacing>, child: I) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    custom_("t-stack")
        .set(child)
        .stylesheet(include_str!("css/stack.css"))


@@ 92,7 96,11 @@ pub fn stack(gap: impl Into<ModularSpacing>, child: impl Into<Node>) -> Node {
/// |   | ...
/// +---+
/// ```
pub fn cluster(gap: impl Into<ModularSpacing>, child: impl Into<Node>) -> Node {
pub fn cluster<I, E>(gap: impl Into<ModularSpacing>, child: I) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    custom_("t-cluster")
        .set(child)
        .stylesheet(include_str!("css/cluster.css"))


@@ 108,7 116,11 @@ pub fn cluster(gap: impl Into<ModularSpacing>, child: impl Into<Node>) -> Node {
/// |///////////|
/// +-----------+
/// ```
pub fn padded(padding: impl Into<ModularSpacing>, child: impl Into<Node>) -> Node {
pub fn padded<I, E>(padding: impl Into<ModularSpacing>, child: I) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    custom_("t-padded")
        .set(child)
        .stylesheet(include_str!("css/padded.css"))


@@ 130,7 142,11 @@ pub fn padded(padding: impl Into<ModularSpacing>, child: impl Into<Node>) -> Nod
/// |            +---+             |
/// +------------------------------+
/// ```
pub fn center(child: impl Into<Node>) -> Node {
pub fn center<I, E>(child: I) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    custom_("t-center")
        .set(child)
        .stylesheet(include_str!("css/center.css"))


@@ 179,20 195,6 @@ pub fn center(child: impl Into<Node>) -> Node {
///
/// The last argument sets the height of the container as a percentage of the viewport width. It
/// defaults to 100.
///
/// The header, footer and main arguments must correspond to exactly one HTML Element.
///
/// So for example, this won't give you what you expect because when a list of Nodes is converted
/// into a single one, it is actually an HTML [fragment](crate::Fragment).
/// ```
/// use toph::{tag::*, component::cover};
///
/// let nope = cover([
///     span_,
///     span_,
///     span_
/// ].into(), None, None, None);
/// ```
pub fn cover(
    main: Node,
    header: Option<Node>,


@@ 234,11 236,15 @@ pub fn cover(
/// |   |
/// +---+
/// ```
pub fn switcher(
pub fn switcher<I, E>(
    gap: impl Into<ModularSpacing>,
    threshold: impl Into<Measure>,
    child: impl Into<Node>,
) -> Node {
    child: I,
) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    custom_("t-switcher")
        .set(child)
        .stylesheet(include_str!("css/switcher.css"))


@@ 275,11 281,15 @@ pub fn switcher(
/// |   |  
/// +---+  
/// ```
pub fn fluid_grid(
pub fn fluid_grid<I, E>(
    ideal_width: impl Into<Measure>,
    gap: impl Into<ModularSpacing>,
    child: impl Into<Node>,
) -> Node {
    child: I,
) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    custom_("t-fluid-grid")
        .set(child)
        .stylesheet(include_str!("css/fluid-grid.css"))


@@ 290,7 300,11 @@ pub fn fluid_grid(
/// A container that acts as a "window" its child element (usually an image)
///
/// The first argument controls the container's aspect ratio.
pub fn frame(ratio: impl Into<Ratio>, child: impl Into<Node>) -> Node {
pub fn frame<I, E>(ratio: impl Into<Ratio>, child: I) -> Node
where
    I: IntoIterator<Item = E>,
    E: Into<Node>,
{
    let ratio = ratio.into().to_string();
    custom_("t-frame")
        .set(child)

M src/lib.rs => src/lib.rs +18 -18
@@ 14,35 14,35 @@
//! use toph::{attr, Node, tag::*};
//!
//! let navigation = [("Home", "/about me"), ("Posts", "/posts")];
//! let mut doc = Node::from([
//! let mut doc = [
//!     doctype_,
//!     html_.with(attr![lang="en"])
//!         .set([
//!             head_.set(title_.set(t_("My Webpage"))),
//!             head_.set([title_.set(["My Webpage"])]),
//!             body_.set([
//!                 ul_.with(attr![id="navigation"])
//!                     .set(
//!                         navigation.into_iter().map(|(caption, url)| {
//!                             li_.set(a_.with(attr![href=url]).set(t_(caption)))
//!                             li_.set([a_.with(attr![href=url]).set([caption])])
//!                         }).collect::<Vec<_>>()
//!                     ),
//!                 h1_.stylesheet("h1 { text-decoration: underline; }")
//!                     .set(t_("My Webpage"))
//!                     .set(["My Webpage"])
//!             ])
//!         ])
//! ]);
//! ];
//!
//! assert_eq!(
//!     doc.write_to_string(true),
//!     Node::render_pretty(doc),
//!     r#"<!DOCTYPE html>
//! <html lang="en">
//!   <head>
//!     <style>
//!       h1 { text-decoration: underline; }
//!     </style>
//!     <title>
//!       My Webpage
//!     </title>
//!     <style>
//!       h1 { text-decoration: underline; }
//!     </style>
//!   </head>
//!   <body>
//!     <ul id="navigation">


@@ 72,27 72,27 @@
//! - Strings are appropriately encoded in HTML, attribute and URL contexts:
//!
//! ```
//! use toph::{attr, tag::*};
//! use toph::{attr, tag::*, Node};
//!
//! let xss_attr_attempt = r#"" onclick="alert(1)""#;
//! let xss_attempt = t_(r#"<script>alert(1)"#);
//! let xss_attempt = r#"<script>alert(1)"#;
//! let url = "/path with space";
//!
//! let mut span = span_
//!     .with(attr![class=xss_attr_attempt])
//!     .set(xss_attempt);
//!     .set([xss_attempt]);
//!
//! let mut anchor = a_
//!     .with(attr![href=url])
//!     .set(t_("A link"));
//!     .set(["A link"]);
//!
//! assert_eq!(
//!     span.write_to_string(false),
//!     Node::render([span]),
//!     r#"<span class="&quot; onclick=&quot;alert(1)&quot;">&lt;script&gt;alert(1)</span>"#
//! );
//!
//! assert_eq!(
//!     anchor.write_to_string(false),
//!     Node::render([anchor]),
//!     r#"<a href="/path%20with%20space">A link</a>"#
//! );
//! ```


@@ 103,7 103,7 @@
//!   - To include files, use [`include_str`]
//!
//! ```
//! use toph::{tag::*};
//! use toph::{tag::*, Node};
//!
//! let user_input = "1rem";
//! let css = format!("p {{ font-size: {}; }}", user_input);


@@ 126,7 126,7 @@
//!     p_.stylesheet(css).var("font-size", user_input),
//! ]);
//! assert_eq!(
//!   html.write_to_string(true),
//!   Node::render_pretty([html]),
//!   r#"<html>
//!   <head>
//!     <style>


@@ 147,4 147,4 @@ pub mod component;
mod encode;
mod node;

pub use node::{tag, Element, Fragment, Node, Text};
pub use node::{tag, Node};

M src/node.rs => src/node.rs +205 -205
@@ 1,4 1,3 @@
mod asset;
pub mod attribute;
pub mod tag;
mod variable;


@@ 6,32 5,52 @@ pub mod visitor;

use crate::encode;
use attribute::AttributeMap;
use std::io;
use variable::CSSVariableMap;

/// An HTML Node
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Node {
    /// See [`Element`]
    Element(Element),
    /// See [`Text`]
    Text(Text),
    /// See [`Fragment`]
    Fragment(Fragment),
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Asset {
    StyleSheet(&'static str),
    JavaScript(&'static str),
}

/// An HTML element. All [tag functions](crate::tag) create an HTML node with this variant
/// An HTML Node. All [tag functions](crate::tag) are instances of this type.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Element {
pub struct Node {
    // When `tag` is not empty, this is an element node
    tag: &'static str,
    // When `tag` is empty, this isa a text node
    text: String,
    attributes: AttributeMap,
    variables: CSSVariableMap,
    child: Option<Box<Node>>,
    assets: Vec<asset::Asset>,
    children: Vec<Node>,
    assets: Vec<Asset>,
}

impl Element {
impl Node {
    const fn element(tag: &'static str) -> Self {
        Node {
            tag,
            text: String::new(),
            attributes: AttributeMap::new(),
            variables: CSSVariableMap::new(),
            children: vec![],
            assets: vec![],
        }
    }

    // NOTE: For consistency, the API should NEVER return a Node with no tag.
    // Text nodes can't have attributes, variables, or children when printed out
    const fn text(text: String) -> Self {
        Node {
            tag: "",
            text,
            attributes: AttributeMap::new(),
            variables: CSSVariableMap::new(),
            children: vec![],
            assets: vec![],
        }
    }

    fn is_void(&self) -> bool {
        matches!(
            self.tag,


@@ 51,65 70,19 @@ impl Element {
                | "!DOCTYPE html"
        )
    }
}

impl Default for Node {
    fn default() -> Self {
        Node::Text(Text("".into()))
    }
}

/// A text element. This is the variant created when a string is given as an argument to a tag
/// function
///
/// Calling the following methods on Node `Text` variant is a no-op:
/// - [`Node::with`]
/// - [`Node::stylesheet`]
/// - [`Node::js`]
/// - [`Node::var`]
/// - [`Node::set`]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Text(String);

/// A Fragment is a container for multiple `Node`s. It's the variant created with an array of
/// nodes is converted to a single node.
///
/// Calling the following methods on Node `Fragment` variant is a no-op:
/// - [`Node::with`]
/// - [`Node::stylesheet`]
/// - [`Node::js`]
/// - [`Node::var`]
/// - [`Node::set`]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Fragment(Vec<Node>);

impl Node {
    /// Converts the tree rooted at this node to an HTML string
    pub fn write_to_string(&mut self, indent: bool) -> String {
        let mut buf = String::new();
        let writer = visitor::HtmlStringWriter::new(&mut buf, indent);
        visitor::visit_nodes(self, writer).expect("printing to a string should not fail");
        buf
    }

    /// Writes the HTML for the tree rooted at this node to anything that implements [`io::Write`]
    pub fn write<W: io::Write>(&mut self, w: W) -> Result<(), io::Error> {
        let writer = visitor::HtmlWriter::new(w);
        visitor::visit_nodes(self, writer)
    }

    /// Sets HTML attributes
    ///
    /// A mix of boolean & regular attributes can be set. You can call this method multiple times
    ///
    /// ```
    /// use toph::{attr, tag::*};
    /// use toph::{attr, tag::*, Node};
    /// let mut s = span_
    ///     .with(attr![class="card", hidden])
    ///     .with(attr![id="hello"]);
    ///
    /// assert_eq!(
    ///     s.write_to_string(false),
    ///     Node::render([s]),
    ///     r#"<span class="card" id="hello" hidden></span>"#
    /// );
    /// ```


@@ 120,9 93,9 @@ impl Node {
    /// Generally, if an attribute appears twice, the last occurence wins
    ///
    /// ```
    /// use toph::{attr, tag::*};
    /// use toph::{attr, tag::*, Node};
    /// assert_eq!(
    ///     span_.with(attr![id="one", id="two"]).write_to_string(false),
    ///     Node::render([span_.with(attr![id="one", id="two"])]),
    ///     r#"<span id="two"></span>"#
    /// );
    /// ```


@@ 130,9 103,9 @@ impl Node {
    /// For space-separated attributes (e..g `class`), occurences are combined with a space;
    ///
    /// ```
    /// use toph::{attr, tag::*};
    /// use toph::{attr, tag::*, Node};
    /// assert_eq!(
    ///     span_.with(attr![class="one", class="two"]).write_to_string(false),
    ///     Node::render([span_.with(attr![class="one", class="two"])]),
    ///     r#"<span class="one two"></span>"#
    /// );
    /// ```


@@ 140,46 113,43 @@ impl Node {
    /// For comma-separated attributes (e.g. `accept`), occurences are combined with a comma;
    ///
    /// ```
    /// use toph::{attr, tag::*};
    /// use toph::{attr, tag::*, Node};
    ///
    /// assert_eq!(
    ///     span_.with(attr![accept="audio/*", accept="video/*"]).write_to_string(false),
    ///     Node::render([span_.with(attr![accept="audio/*", accept="video/*"])]),
    ///     r#"<span accept="audio/*,video/*"></span>"#
    /// );
    /// ```
    ///
    /// See the [attr](crate::attr) macro docs for details.
    pub fn with<I>(mut self, attributes: I) -> Node
    pub fn with<I>(mut self, attributes: I) -> Self
    where
        I: IntoIterator<Item = (&'static str, String, bool)>,
    {
        if let Self::Element(ref mut el) = self {
            for attr in attributes {
                el.attributes.insert(attr.0, &attr.1, attr.2)
            }
        for attr in attributes {
            self.attributes.insert(attr.0, &attr.1, attr.2)
        }
        self
    }

    /// Links an inline css stylesheet to the Node
    ///
    /// The stylesheet will be included verbatim in a `<style>` element when this Node is in a tree with
    /// both `<html>` & `<head>` tags
    /// The stylesheet will be included verbatim in a `<style>` element when this Node is in a tree
    /// with both `<html>` & `<head>` tags
    ///
    /// # Example
    ///
    /// ```
    /// use toph::{tag::*};
    /// use toph::{tag::*, Node};
    ///
    /// let mut html = div_.stylesheet("div { border: 1px solid black; }");
    /// assert_eq!(html.write_to_string(false), "<div></div>");
    /// let html = div_.stylesheet("div { border: 1px solid black; }");
    /// assert_eq!(Node::render([html]), "<div></div>");
    ///
    /// let mut html = html_.set([
    /// let html = html_.set([
    ///     head_,
    ///     div_.stylesheet("div { border: 1px solid black; }")
    /// ]);
    /// assert_eq!(
    ///     html.write_to_string(false),
    ///     Node::render([html]),
    ///     "<html><head><style>div { border: 1px solid black; }</style></head><div></div></html>"
    /// );
    /// ```


@@ 187,32 157,30 @@ impl Node {
    ///
    /// CSS snippets are de-duplicated; Including the same snippet multiple times  will
    /// still result in a single `<style>` element
    pub fn stylesheet(mut self, css: &'static str) -> Node {
        if let Self::Element(ref mut el) = self {
            el.assets.push(asset::Asset::StyleSheet(css));
        }
    pub fn stylesheet(mut self, css: &'static str) -> Self {
        self.assets.push(Asset::StyleSheet(css));
        self
    }

    /// Links a JavaScript snippet to the Node
    ///
    /// The javascript snippet will be included verbatim as a `<script>` element when this Node is in a tree
    /// with both `<html>` & `<body>` tags
    /// The javascript snippet will be included verbatim as a `<script>` element when this Node is
    /// in a tree with both `<html>` & `<body>` tags
    ///
    /// # Example
    ///
    /// ```
    /// use toph::{tag::*};
    /// use toph::{tag::*, Node};
    ///
    /// let mut html = div_.js("console.log()");
    /// assert_eq!(html.write_to_string(false), "<div></div>");
    /// let html = div_.js("console.log()");
    /// assert_eq!(Node::render([html]), "<div></div>");
    ///
    /// let mut html = html_.set([
    ///     body_.set(div_.js("console.log()"))
    /// let html = html_.set([
    ///     body_.set([div_.js("console.log()")])
    /// ]);
    ///
    /// assert_eq!(
    ///     html.write_to_string(false),
    ///     Node::render([html]),
    ///     "<html><body><div></div><script>console.log()</script></body></html>"
    /// );
    /// ```


@@ 221,10 189,8 @@ impl Node {
    ///
    /// JavaScript snippets are de-duplicated; Including the same snippet multiple times  will
    /// still result in a single `<script>` element
    pub fn js(mut self, js: &'static str) -> Node {
        if let Self::Element(ref mut el) = self {
            el.assets.push(asset::Asset::JavaScript(js));
        }
    pub fn js(mut self, js: &'static str) -> Self {
        self.assets.push(Asset::JavaScript(js));
        self
    }



@@ 235,10 201,10 @@ impl Node {
    ///
    /// # Example
    /// ```
    /// use toph::{tag::*};
    /// use toph::{tag::*, Node};
    ///
    /// let css = "div { color: var(--text-color); border: 1px solid var(--div-color); }";
    /// let mut html = html_.set([
    /// let html = html_.set([
    ///     head_,
    ///     body_.set([
    ///         div_.stylesheet(css)


@@ 252,7 218,7 @@ impl Node {
    /// ]);
    ///
    /// assert_eq!(
    ///     html.write_to_string(true),
    ///     Node::render_pretty([html]),
    /// r#"<html>
    ///   <head>
    ///     <style>


@@ 272,120 238,134 @@ impl Node {
    /// # Notes:
    /// - Double dashes are automatically prepended to the name when displayed
    /// - The value is always attribute encoded
    pub fn var(mut self, name: &'static str, value: &str) -> Node {
        if let Self::Element(ref mut el) = self {
            el.variables.insert(name, value)
        }
    pub fn var(mut self, name: &'static str, value: &str) -> Self {
        self.variables.insert(name, value);
        self
    }

    /// Sets this Element's children
    ///
    ///
    /// You can pass in another `Node` as an argument, as well as anything that can be [converted into
    /// an iterator of `Nodes`](std::iter::IntoIterator)
    /// You can pass anything that can be converted into an iterator of `Nodes`
    ///
    /// This includes things such as `Option<Node>`, `Result<Node>` and arrays & `Vec`s of `Node`s
    ///
    /// # Examples
    ///
    /// ```
    /// use toph::tag::*;
    ///
    /// // Another node
    /// span_.set(t_("hello"));
    /// // Strings can be converted to nodes. So an array of strings works
    /// span_.set(["hello", "world"]);
    ///
    /// // An array of nodes
    /// span_.set([div_, span_]);
    ///
    /// // if you want to mix them, you have to explicitely use `.into()` on the string
    /// span_.set([div_, "hey".into(), div_]);
    ///
    /// // A node wrapped in an  option or result
    /// span_.set(Some(div_));
    /// ```
    pub fn set(mut self, child: impl Into<Node>) -> Node {
        if let Self::Element(ref mut el) = self {
            el.child = Some(Box::new(child.into()));
            if el.tag == "html" {
                visitor::include_assets(&mut self);
            }
        }
    ///
    /// Calling this multiple times appends children
    pub fn set<I, E>(mut self, children: I) -> Self
    where
        I: IntoIterator<Item = E>,
        E: Into<Node>,
    {
        let mut children = children.into_iter().map(|c| c.into()).collect::<Vec<_>>();
        self.children.append(&mut children);
        self
    }

    /// Set this element's content without html encoding
    /// Append `html` verbatim as a child of this element. This skip html encoding.
    ///
    /// ```
    /// use toph::tag::*;
    /// let mut html = span_.dangerously_set_html("<script>alert(1)</script>");
    /// use toph::{Node, tag::*};
    /// let html = span_.dangerously_set_html("<script>alert(1)</script>");
    ///
    /// assert_eq!(
    ///     html.write_to_string(false),
    ///     Node::render([html]),
    ///     "<span><script>alert(1)</script></span>"
    /// );
    /// ```
    pub fn dangerously_set_html(mut self, html: &str) -> Node {
        if let Self::Element(ref mut el) = self {
            el.child = Some(Box::new(Node::Text(Text(html.to_string()))))
        }
    pub fn dangerously_set_html(mut self, html: &str) -> Self {
        let child_text_element = Node::text(html.into());
        self.children.push(child_text_element);
        self
    }
}
impl Node {
    /// Converts the list of nodes to an HTML string
    ///
    /// If one of the nodes is an `<html>` element, css & javascript will be extracted from its
    /// children and appended to the `<head>` & `<body>` elements respectively , if they
    /// exist
    pub fn render<I, N>(nodes: I) -> String
    where
        I: IntoIterator<Item = N>,
        N: Into<Node>,
    {
        Node::render_to_string(nodes, false)
    }

    /// Converts the list of nodes to an HTML string with indentation
    ///
    /// See [`render`](`crate::Node::render`)
    pub fn render_pretty<I, N>(nodes: I) -> String
    where
        I: IntoIterator<Item = N>,
        N: Into<Node>,
    {
        Node::render_to_string(nodes, true)
    }

    fn render_to_string<I, N>(nodes: I, indent: bool) -> String
    where
        I: IntoIterator<Item = N>,
        N: Into<Node>,
    {
        let mut buf = String::new();
        for node in nodes {
            let mut node = node.into();
            if node.tag == "html" {
                visitor::include_assets(&mut node);
            }

            let writer = visitor::HtmlStringWriter::new(&mut buf, indent);
            visitor::visit_nodes(&mut node, writer).expect("printing to a string should not fail");
        }
        buf
    }
}

// impl From<&str> for Node {
//     fn from(value: &str) -> Self {
//         Node::from(value.to_string())
//     }
// }

// impl From<String> for Node {
//     fn from(value: String) -> Self {
//         let encoded = encode::html(&value);
//         Node::Text(Text(encoded))
//     }
// }

//impl<I: Into<Node>> From<Option<I>> for Node {
//    fn from(value: Option<I>) -> Self {
//        value.map(|v| v.into()).unwrap_or_default()
//    }
//}

// impl<I> From<Vec<I>> for Node
// where
//     I: Into<Node>,
// {
//     fn from(value: Vec<I>) -> Self {
//         let nodes = value.into_iter().map(|v| v.into()).collect::<Vec<_>>();
//         Self::Fragment(Fragment(nodes))
//     }
// }

impl<I> From<I> for Node
where
    I: IntoIterator<Item = Node>,
{
    fn from(value: I) -> Self {
        let vec = value.into_iter().collect::<Vec<_>>();
        Self::Fragment(Fragment(vec))
impl From<&str> for Node {
    fn from(value: &str) -> Self {
        Node::text(encode::html(value))
    }
}

// macro_rules! impl_node_for_array_of_nodes {
//     ($($n:expr),+) => {
//         $(
//             impl<I: Into<Node>> From<[I; $n]> for Node {
//                 fn from(value: [I; $n]) -> Self {
//                     let nodes = value.into_iter().map(|v| v.into()).collect::<Vec<_>>();
//                     Node::Fragment(Fragment(nodes))
//                 }
//             }
//         )+
//     };
// }
//
// #[rustfmt::skip]
// impl_node_for_array_of_nodes!(
//     1, 2, 3, 4, 5, 6, 7, 8, 9,
//     10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
//     20
// );
impl From<String> for Node {
    fn from(value: String) -> Self {
        Node::from(value.as_str())
    }
}

impl<I: Into<Node>, E> From<Result<I, E>> for Node {
    fn from(value: Result<I, E>) -> Self {
        value
            .map(|e| e.into())
            .unwrap_or_else(|_| Node::text(String::new()))
    }
}

impl<I: Into<Node>> From<Option<I>> for Node {
    fn from(value: Option<I>) -> Self {
        value
            .map(|e| e.into())
            .unwrap_or_else(|| Node::text(String::new()))
    }
}

#[cfg(test)]
mod tests {


@@ 394,73 374,75 @@ mod tests {
    use crate::node::tag::*;

    #[track_caller]
    fn assert_html(node: impl Into<Node>, expected: &str) {
        assert_eq!((&mut node.into()).write_to_string(false), expected);
    fn assert_html<I, N>(nodes: I, expected: &str)
    where
        I: IntoIterator<Item = N>,
        N: Into<Node>,
    {
        let actual = Node::render(nodes);
        assert_eq!(actual, expected);
    }

    #[test]
    fn html_fragments() {
        // including strings
        assert_html(
            [
                span_.set(t_("literal")),
                span_.set(t_(String::from("string"))),
            ],
            [span_.set(["literal"]), span_.set([String::from("string")])],
            "<span>literal</span><span>string</span>",
        );

        // strings are html encoded
        assert_html(span_.set(t_("<script>")), "<span>&lt;script&gt;</span>");
        assert_html([span_.set(["<script>"])], "<span>&lt;script&gt;</span>");

        // nesting nodes
        assert_html(
            [div_.set([span_, div_.set(div_)])],
            [div_.set([span_, div_.set([div_])])],
            "<div><span></span><div><div></div></div></div>",
        );

        // regular attributes
        assert_html(
            span_.with(attr![onclick = "something"]),
            [span_.with(attr![onclick = "something"])],
            r#"<span onclick="something"></span>"#,
        );
        assert_html(
            span_.with(attr![onclick = String::from("something")]),
            [span_.with(attr![onclick = String::from("something")])],
            r#"<span onclick="something"></span>"#,
        );

        // boolean attributes are supported
        assert_html(span_.with(attr![async]), "<span async></span>");
        assert_html([span_.with(attr![async])], "<span async></span>");

        // mix of regular & boolean attributes
        assert_html(
            span_.with(attr![async, class = "hidden", checked]),
            [span_.with(attr![async, class = "hidden", checked])],
            r#"<span class="hidden" async checked></span>"#,
        );
        assert_html(
            span_.with(attr![class = "hidden", async, id = "id"]),
            [span_.with(attr![class = "hidden", async, id = "id"])],
            r#"<span class="hidden" id="id" async></span>"#,
        );

        // optional comma at the end of attribute list
        assert_html(span_.with(attr![async,]), "<span async></span>");
        assert_html([span_.with(attr![async,])], "<span async></span>");
        assert_html(
            span_.with(attr![class = "class",]),
            [span_.with(attr![class = "class",])],
            r#"<span class="class"></span>"#,
        );

        // data-* attributes are supported
        assert_html(
            span_.with(attr![data_custom = "hello"]),
            [span_.with(attr![data_custom = "hello"])],
            r#"<span data-custom="hello"></span>"#,
        );
    }

    #[test]
    fn including_assets() {
        // css is prepended to the head element
        // css is appended to the head element
        assert_html(
            [html_.set([head_.set(title_), body_.stylesheet("some css")])],
            r#"<html><head><style>some css</style><title></title></head><body></body></html>"#,
            [html_.set([head_.set([title_]), body_.stylesheet("some css")])],
            r#"<html><head><title></title><style>some css</style></head><body></body></html>"#,
        );
        // css is added if when head element is empty
        assert_html(


@@ 469,31 451,49 @@ mod tests {
        );
        // no css is included when head is absent
        assert_html(
            [html_.set(body_.stylesheet("some css"))],
            [html_.set([body_.stylesheet("some css")])],
            "<html><body></body></html>",
        );
        // no css is included when html is absent
        assert_html([body_.stylesheet("some css")], "<body></body>");

        // css is deduplicated
        assert_html(
            [html_.stylesheet("a").set([head_, body_.stylesheet("a")])],
            "<html><head><style>a</style></head><body></body></html>",
        );

        // js is appended to the body element
        assert_html(
            [html_.set(body_.js("some js").set(span_))],
            [html_.set([body_.js("some js").set([span_])])],
            "<html><body><span></span><script>some js</script></body></html>",
        );

        // js is added when body element is empty
        assert_html(
            [html_.set(body_.js("some js"))],
            [html_.set([body_.js("some js")])],
            "<html><body><script>some js</script></body></html>",
        );

        // no js is added when body is absent
        assert_html(
            [html_.set(span_.js("some js"))],
            [html_.set([span_.js("some js")])],
            "<html><span></span></html>",
        );

        // no js is added when html is absent
        assert_html([body_.js("some js")], "<body></body>");

        // js is deduplicated
        assert_html(
            [html_.js("js").set([body_.js("js")])],
            "<html><body><script>js</script></body></html>",
        );

        // order in which assets are appended does not matter
        assert_html(
            [html_.set([head_, body_]).js("js").stylesheet("css")],
            "<html><head><style>css</style></head><body><script>js</script></body></html>",
        );
    }
}

D src/node/asset.rs => src/node/asset.rs +0 -7
@@ 1,7 0,0 @@
// Element assets refer to CSS and Javascript associated with a given element

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Asset {
    StyleSheet(&'static str),
    JavaScript(&'static str),
}

M src/node/attribute.rs => src/node/attribute.rs +3 -3
@@ 233,7 233,7 @@ macro_rules! attr_impl {
#[cfg(test)]
mod tests {
    use super::*;
    use crate::tag::*;
    use crate::{tag::*, Node};

    #[test]
    fn inserting_comma_separated_attributes() {


@@ 315,10 315,10 @@ mod tests {

    #[test]
    fn attributes_with_underscores() {
        let mut html = span_.with(attr![data_hello = "hi", something_something]);
        let html = span_.with(attr![data_hello = "hi", something_something]);

        assert_eq!(
            html.write_to_string(false),
            Node::render([html]),
            r#"<span data-hello="hi" something-something></span>"#
        );
    }

M src/node/tag.rs => src/node/tag.rs +6 -50
@@ 6,28 6,12 @@
//! You can also create an HTML element [with a custom tag name](crate::tag::custom_).
//!
//! Missing from this module are constants for the `_script` & `_style` elements. JavaScript & CSS
//! snippets are set using [`Node::with`](crate::Node::with) and the
//! [`attr`](crate::attr#js--css-snippets) macro

//! snippets are set using [`Node::js`] and [`Node::stylesheet`] respectively
use super::*;
use attribute::AttributeMap;
use encode::html;
use variable::CSSVariableMap;

/// Creates an HTML Node with a custom tag name.
pub fn custom_(tag: &'static str) -> Node {
    Node::Element(Element {
        tag,
        child: None,
        attributes: AttributeMap::new(),
        assets: vec![],
        variables: CSSVariableMap::new(),
    })
}

/// Creates a plain HTML text element
pub fn t_<I: AsRef<str>>(text: I) -> Node {
    Node::Text(Text(html(text.as_ref())))
    Node::element(tag)
}

macro_rules! impl_tag {


@@ 40,49 24,21 @@ macro_rules! impl_tag {
        paste::paste!{
            #[allow(non_upper_case_globals)]
            #[doc = $doc]
            pub const [<$tag _>]: Node = Node::Element(Element {
                tag: stringify!($tag),
                attributes: AttributeMap::new(),
                assets: vec![],
                child: None,
                variables: CSSVariableMap::new()
            });
            pub const [<$tag _>]: Node = Node::element(stringify!($tag));
        }
    }
}

/// The <!DOCTYPE> element
#[allow(non_upper_case_globals)]
pub const doctype_: Node = Node::Element(Element {
    tag: "!DOCTYPE html",
    attributes: AttributeMap::new(),
    child: None,
    assets: vec![],
    variables: CSSVariableMap::new(),
});
pub const doctype_: Node = Node::element("!DOCTYPE html");

// script_ & style_ tag constants are omitted from the public API
#[allow(non_upper_case_globals)]
pub(crate) const script_: Node = Node::Element(Element {
    tag: "script",
    attributes: AttributeMap::new(),
    assets: vec![],
    child: None,
    variables: CSSVariableMap::new(),
});

/// An empty Node that does not render as anything
#[allow(non_upper_case_globals)]
pub const empty_: Node = Node::Text(Text(String::new()));
pub(crate) const script_: Node = Node::element("script");

#[allow(non_upper_case_globals)]
pub(crate) const style_: Node = Node::Element(Element {
    tag: "style",
    attributes: AttributeMap::new(),
    child: None,
    assets: vec![],
    variables: CSSVariableMap::new(),
});
pub(crate) const style_: Node = Node::element("style");

#[rustfmt::skip]
impl_tag![

M src/node/visitor.rs => src/node/visitor.rs +22 -100
@@ 1,13 1,11 @@
use super::{asset::Asset, tag::*, Element, Node, Text};
use super::{tag::*, Asset, Node};
use std::borrow::Cow;
use std::collections::btree_map::Entry;
use std::collections::HashSet;
use std::fmt;
use std::io;
use std::mem;

enum Tag<'n> {
    Open(Option<&'n mut Node>),
    Open(&'n mut Node),
    Close(&'static str),
}



@@ 18,9 16,6 @@ pub fn include_assets(node: &mut Node) {
    let mut collector = SnippetCollector::new();
    visit_nodes(node, &mut collector).expect("collecting assets does not fail");

    let mut style = None;
    let mut script = None;

    let script_fragments = collector
        .js
        .into_iter()


@@ 32,16 27,8 @@ pub fn include_assets(node: &mut Node) {
        .map(|c| style_.dangerously_set_html(c))
        .collect::<Vec<_>>();

    if !script_fragments.is_empty() {
        script = Some(script_fragments.into());
    }

    if !style_fragments.is_empty() {
        style = Some(style_fragments.into());
    }

    // Insert them into the tree
    let inserter = AssetInserter::new(style, script);
    let inserter = AssetInserter::new(style_fragments, script_fragments);
    visit_nodes(node, inserter).expect("inserting nodes does not fail");
}



@@ 49,7 36,7 @@ pub fn include_assets(node: &mut Node) {
// [1]: https://rust-unofficial.github.io/patterns/patterns/behavioural/visitor.html
pub trait NodeVisitor {
    type Error;
    fn visit_open_tag(&mut self, _el: &mut Element) -> Result<(), Self::Error> {
    fn visit_open_tag(&mut self, _el: &mut Node) -> Result<(), Self::Error> {
        Ok(())
    }
    fn visit_close_tag(&mut self, _tag: &'static str) -> Result<(), Self::Error> {


@@ 74,11 61,16 @@ pub fn visit_nodes<V: NodeVisitor>(
    mut visitor: V,
) -> Result<(), <V as NodeVisitor>::Error> {
    let mut visit_later: Vec<Tag> = vec![];
    visit_later.push(Tag::Open(Some(start)));
    visit_later.push(Tag::Open(start));

    while let Some(t) = visit_later.pop() {
        match t {
            Tag::Open(Some(Node::Element(el))) => {
            Tag::Open(el) => {
                if el.tag.is_empty() {
                    visitor.visit_text(&el.text)?;
                    continue;
                }

                visitor.visit_open_tag(el)?;

                if el.is_void() {


@@ 88,20 80,13 @@ pub fn visit_nodes<V: NodeVisitor>(
                // re-visit this node after its children have been visited
                visit_later.push(Tag::Close(el.tag));

                visit_later.push(Tag::Open(el.child.as_deref_mut()));
                for child in el.children.iter_mut().rev() {
                    visit_later.push(Tag::Open(child));
                }
            }
            Tag::Close(tag_name) => {
                visitor.visit_close_tag(tag_name)?;
            }
            Tag::Open(Some(Node::Fragment(f))) => {
                for child in f.0.iter_mut().rev() {
                    visit_later.push(Tag::Open(Some(child)));
                }
            }
            Tag::Open(Some(Node::Text(Text(ref t)))) => {
                visitor.visit_text(t)?;
            }
            _ => {}
        }
    }



@@ 167,7 152,7 @@ impl<W: fmt::Write> HtmlStringWriter<W> {
impl<W: fmt::Write> NodeVisitor for HtmlStringWriter<W> {
    type Error = fmt::Error;

    fn visit_open_tag(&mut self, el: &mut Element) -> Result<(), Self::Error> {
    fn visit_open_tag(&mut self, el: &mut Node) -> Result<(), Self::Error> {
        write!(self.html, "{}<{}", self.current_indent(), el.tag)?;
        // css variables are set using the `style` attribute
        // merge them with any existing style attribute


@@ 215,63 200,14 @@ impl<W: fmt::Write> NodeVisitor for HtmlStringWriter<W> {
    }
}

// A visitor that transforms a Node tree to an html byte stream.
pub struct HtmlWriter<W> {
    html: W,
}
impl<W: io::Write> HtmlWriter<W> {
    pub fn new(inner: W) -> Self {
        Self { html: inner }
    }
}

impl<W: io::Write> NodeVisitor for HtmlWriter<W> {
    type Error = io::Error;

    fn visit_open_tag(&mut self, el: &mut Element) -> Result<(), Self::Error> {
        write!(self.html, "<{}", el.tag)?;
        // css variables are set using the `style` attribute
        // merge them with any existing style attribute
        if !el.variables.is_empty() {
            match el.attributes.entry("style") {
                Entry::Vacant(v) => {
                    v.insert(el.variables.to_string());
                }
                Entry::Occupied(mut o) => {
                    let existing = o.get_mut();
                    *existing += &el.variables.to_string();
                }
            }
        }
        write!(self.html, "{}", el.attributes)?;
        write!(self.html, ">")?;
        Ok(())
    }

    fn visit_close_tag(&mut self, tag: &'static str) -> Result<(), Self::Error> {
        write!(self.html, "</{}>", tag)?;
        Ok(())
    }

    fn visit_text(&mut self, text: &str) -> Result<(), Self::Error> {
        write!(self.html, "{}", text)?;
        Ok(())
    }

    fn finish(&mut self) -> Result<(), Self::Error> {
        self.html.flush()?;
        Ok(())
    }
}

// A visitor that inserts style & script nodes into a node tree
pub struct AssetInserter {
    style: Option<Node>,
    script: Option<Node>,
    style: Vec<Node>,
    script: Vec<Node>,
}

impl AssetInserter {
    pub fn new(style: Option<Node>, script: Option<Node>) -> Self {
    pub fn new(style: Vec<Node>, script: Vec<Node>) -> Self {
        Self { style, script }
    }
}


@@ 279,25 215,11 @@ impl AssetInserter {
impl NodeVisitor for AssetInserter {
    type Error = ();

    fn visit_open_tag(&mut self, el: &mut Element) -> Result<(), Self::Error> {
    fn visit_open_tag(&mut self, el: &mut Node) -> Result<(), Self::Error> {
        if el.tag == "head" {
            if let Some(node) = self.style.take() {
                if let Some(mut old) = el.child.take() {
                    *old = [node, mem::take(&mut old)].into();
                    el.child = Some(old);
                } else {
                    el.child = Some(Box::new(node));
                }
            }
            el.children.append(&mut self.style);
        } else if el.tag == "body" {
            if let Some(node) = self.script.take() {
                if let Some(mut old) = el.child.take() {
                    *old = [mem::take::<Node>(&mut old), node].into();
                    el.child = Some(old);
                } else {
                    el.child = Some(Box::new(node));
                }
            }
            el.children.append(&mut self.script);
        }

        Ok(())


@@ 322,7 244,7 @@ impl SnippetCollector {
impl NodeVisitor for &mut SnippetCollector {
    type Error = ();

    fn visit_open_tag(&mut self, el: &mut Element) -> Result<(), Self::Error> {
    fn visit_open_tag(&mut self, el: &mut Node) -> Result<(), Self::Error> {
        for asset in el.assets.iter_mut() {
            match asset {
                Asset::StyleSheet(css) => {