use crate::core::{Element, Node};
#[macro_export]
macro_rules! html {
($($body:tt)*) => {{
let mut __nodes = $crate::macros::node_buffer();
$crate::html_nodes!(__nodes, $($body)*);
if __nodes.len() == 1 {
__nodes.pop().unwrap_or_else(|| $crate::Node::Fragment(::std::vec::Vec::new()))
} else {
$crate::Node::Fragment(__nodes)
}
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! html_nodes {
($out:ident,) => {};
($out:ident, @if $($tail:tt)*) => { $crate::html_if!($out, [] $($tail)*); };
($out:ident, @for $($tail:tt)*) => { $crate::html_for!($out, [] $($tail)*); };
($out:ident, raw($value:expr) $($rest:tt)*) => {
$out.push($crate::Node::raw(::std::string::ToString::to_string(&$value)));
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, $tag:ident ( $($attrs:tt)* ) { $($children:tt)* } $($rest:tt)*) => {
{
let __el = $crate::elements::$tag();
let __el = $crate::html_attrs!(__el, $($attrs)*);
let mut __kids = $crate::macros::node_buffer();
$crate::html_nodes!(__kids, $($children)*);
$out.push($crate::Node::Element(
$crate::macros::element_with_children(__el, __kids)));
}
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, $tag:ident ( $($attrs:tt)* ) $($rest:tt)*) => {
{
let __el = $crate::elements::$tag();
$out.push($crate::Node::Element($crate::html_attrs!(__el, $($attrs)*)));
}
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, $tag:ident { $($children:tt)* } $($rest:tt)*) => {
{
let mut __kids = $crate::macros::node_buffer();
$crate::html_nodes!(__kids, $($children)*);
$out.push($crate::Node::Element(
$crate::macros::element_with_children($crate::elements::$tag(), __kids)));
}
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, ($value:expr) $($rest:tt)*) => {
$out.push($crate::Node::text(::std::string::ToString::to_string(&$value)));
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, $text:literal $($rest:tt)*) => {
$out.push($crate::Node::text($text));
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, $tag:ident $($rest:tt)*) => {
$out.push($crate::Node::Element($crate::elements::$tag()));
$crate::html_nodes!($out, $($rest)*);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! html_attrs {
($el:expr,) => { $el };
($el:expr) => { $el };
($el:expr, $key:literal = $value:expr, $($rest:tt)*) => {
$crate::html_attrs!(
$el.attr($key, ::std::string::ToString::to_string(&$value)), $($rest)*)
};
($el:expr, $key:literal = $value:expr) => {
$el.attr($key, ::std::string::ToString::to_string(&$value))
};
($el:expr, $key:ident = $value:expr, $($rest:tt)*) => {
$crate::html_attrs!(
$el.attr(stringify!($key), ::std::string::ToString::to_string(&$value)), $($rest)*)
};
($el:expr, $key:ident = $value:expr) => {
$el.attr(stringify!($key), ::std::string::ToString::to_string(&$value))
};
($el:expr, $key:literal, $($rest:tt)*) => {
$crate::html_attrs!($el.bool_attr($key), $($rest)*)
};
($el:expr, $key:literal) => {
$el.bool_attr($key)
};
($el:expr, $key:ident, $($rest:tt)*) => {
$crate::html_attrs!($el.bool_attr(stringify!($key)), $($rest)*)
};
($el:expr, $key:ident) => {
$el.bool_attr(stringify!($key))
};
}
#[doc(hidden)]
#[must_use]
pub fn node_buffer() -> Vec<Node> {
Vec::new()
}
#[doc(hidden)]
#[must_use]
pub fn element_with_children(element: Element, children: Vec<Node>) -> Element {
let all_text = !children.is_empty() && children.iter().all(|c| matches!(c, Node::Text(_)));
if all_text {
let mut content = String::new();
for child in children {
if let Node::Text(text) = child {
content.push_str(&text);
}
}
return element.raw_text(content);
}
element.children_from(children)
}
#[doc(hidden)]
#[macro_export]
macro_rules! html_if {
($out:ident, [$($cond:tt)+] { $($body:tt)* } @else { $($alt:tt)* } $($rest:tt)*) => {
if $($cond)+ { $crate::html_nodes!($out, $($body)*); }
else { $crate::html_nodes!($out, $($alt)*); }
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, [$($cond:tt)+] { $($body:tt)* } $($rest:tt)*) => {
if $($cond)+ { $crate::html_nodes!($out, $($body)*); }
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, [$($cond:tt)*] $next:tt $($rest:tt)*) => {
$crate::html_if!($out, [$($cond)* $next] $($rest)*);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! html_for {
($out:ident, [$($head:tt)+] { $($body:tt)* } $($rest:tt)*) => {
for $($head)+ { $crate::html_nodes!($out, $($body)*); }
$crate::html_nodes!($out, $($rest)*);
};
($out:ident, [$($head:tt)*] $next:tt $($rest:tt)*) => {
$crate::html_for!($out, [$($head)* $next] $($rest)*);
};
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[test]
fn a_bare_element_renders_empty() {
assert_eq!(html! { div }.render(), "<div></div>");
}
#[test]
fn text_children_are_escaped() {
assert_eq!(html! { p { "a & b" } }.render(), "<p>a & b</p>");
}
#[test]
fn raw_children_are_not_escaped() {
assert_eq!(html! { p { raw("<b>x</b>") } }.render(), "<p><b>x</b></p>");
}
#[test]
fn attributes_and_children_combine() {
assert_eq!(
html! { div(class = "card", id = "hero") { p { "hi" } } }.render(),
r#"<div class="card" id="hero"><p>hi</p></div>"#
);
}
#[test]
fn a_bare_attribute_name_is_a_boolean_attribute() {
assert_eq!(
html! { input("type" = "email", name = "email", required) }.render(),
r#"<input type="email" name="email" required>"#
);
}
#[test]
fn expressions_interpolate_escaped() {
let name = "Tom & Jerry";
assert_eq!(
html! { span { (name) } }.render(),
"<span>Tom & Jerry</span>"
);
}
#[test]
fn for_loops_expand_to_siblings() {
let names = ["Ana", "Bruno"];
assert_eq!(
html! { ul { @for n in names { li { (n) } } } }.render(),
"<ul><li>Ana</li><li>Bruno</li></ul>"
);
}
#[test]
fn a_false_condition_contributes_no_node() {
let show = false;
assert_eq!(
html! { div { @if show { p { "x" } } } }.render(),
"<div></div>"
);
}
#[test]
fn if_else_picks_one_branch() {
let logged_in = true;
let markup = html! {
nav { @if logged_in { a { "Sign out" } } @else { a { "Sign in" } } }
};
assert_eq!(markup.render(), "<nav><a>Sign out</a></nav>");
}
#[test]
fn several_roots_become_a_fragment() {
assert_eq!(html! { p { "a" } p { "b" } }.render(), "<p>a</p><p>b</p>");
}
#[test]
fn macro_output_equals_the_builder_output() {
let from_macro = html! { div(class = "card") { h1 { "T" } p { "B" } } };
let from_builder = Node::from(
div()
.add_class("card")
.child(h1().text("T"))
.child(p().text("B")),
);
assert_eq!(from_macro.render(), from_builder.render());
assert_eq!(from_macro.render_pretty(), from_builder.render_pretty());
}
#[test]
fn nesting_survives_pretty_printing() {
let markup = html! { div { section { p { "deep" } } } };
assert_eq!(
markup.render_pretty(),
"<div>\n <section>\n <p>deep</p>\n </section>\n</div>"
);
}
}