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>"
);
}
#[test]
fn the_macro_and_the_builder_agree_on_a_plain_container() {
let macro_built = html! { div { p { "Hi" } } };
let builder_built = div().child(p().text("Hi"));
assert_eq!(macro_built.render(), builder_built.render());
assert_eq!(macro_built.render(), "<div><p>Hi</p></div>");
}
#[test]
fn the_macro_and_the_builder_agree_on_a_required_attribute() {
let macro_built = html! { a(href = "/docs") { span { "Docs" } } };
let builder_built = link_to("/docs").child(span().text("Docs"));
assert_eq!(macro_built.render(), builder_built.render());
assert_eq!(
macro_built.render(),
r#"<a href="/docs"><span>Docs</span></a>"#
);
}
#[test]
fn attributes_and_children_render_together() {
assert_eq!(
html! { div(id = "main") { p { "Hi" } } }.render(),
r#"<div id="main"><p>Hi</p></div>"#
);
}
#[test]
fn a_boolean_flag_survives_the_macro() {
assert_eq!(
html! { details(open) { summary { "More" } } }.render(),
"<details open><summary>More</summary></details>"
);
}
#[test]
fn the_table_family_nests() {
let markup = html! {
table {
thead { tr { th { "A" } } }
tbody { tr { td { "1" } } }
}
};
assert_eq!(
markup.render(),
"<table><thead><tr><th>A</th></tr></thead><tbody><tr><td>1</td></tr></tbody></table>"
);
}
#[test]
fn the_form_family_nests() {
let markup = html! {
form {
fieldset {
legend { "Account" }
label("for" = "email") { "Email" }
}
}
};
assert_eq!(
markup.render(),
concat!(
"<form><fieldset><legend>Account</legend>",
r#"<label for="email">Email</label></fieldset></form>"#,
)
);
}
#[test]
fn the_media_family_nests() {
let markup = html! {
picture {
source(srcset = "a.webp", "type" = "image/webp")
img(src = "a.jpg", alt = "A")
}
};
assert_eq!(
markup.render(),
concat!(
r#"<picture><source srcset="a.webp" type="image/webp">"#,
r#"<img src="a.jpg" alt="A"></picture>"#,
)
);
}
#[test]
fn a_loop_inside_the_macro_repeats_its_body() {
let markup = html! { ul { @for name in ["a", "b", "c"] { li { (name) } } } };
assert_eq!(markup.render(), "<ul><li>a</li><li>b</li><li>c</li></ul>");
}
#[test]
fn a_false_condition_inside_the_macro_renders_nothing() {
let is_admin = false;
let markup = html! {
nav {
a(href = "/") { "Home" }
@if is_admin { a(href = "/admin") { "Admin" } }
}
};
assert_eq!(markup.render(), r#"<nav><a href="/">Home</a></nav>"#);
}
#[test]
fn a_mapped_sequence_matches_the_macro_loop() {
let macro_built = html! { ol { @for item in ["x", "y"] { li { (item) } } } };
let builder_built = ol().children_from(["x", "y"].map(|item| li().text(item)));
assert_eq!(macro_built.render(), builder_built.render());
assert_eq!(macro_built.render(), "<ol><li>x</li><li>y</li></ol>");
}
#[test]
fn an_empty_body_produces_an_empty_element() {
assert_eq!(html! { div {} }.render(), "<div></div>");
}
#[test]
fn an_untyped_element_takes_macro_built_children() {
let group = Element::new("hgroup")
.child(html! { h1 { "Title" } })
.child(html! { p { "Subtitle" } });
assert_eq!(
group.render(),
"<hgroup><h1>Title</h1><p>Subtitle</p></hgroup>"
);
}
#[test]
fn chaining_happens_on_the_builder_side() {
let chained = div().child(html! { p { "Hi" } }).add_class("card");
assert_eq!(chained.render(), r#"<div class="card"><p>Hi</p></div>"#);
}
#[test]
fn nesting_goes_as_deep_as_it_is_written() {
let page = html! {
body { main_tag { section { article { h2 { "Title" } } } } }
};
assert_eq!(
page.render(),
"<body><main><section><article><h2>Title</h2></article></section></main></body>"
);
}
#[test]
fn a_root_html_element_keeps_its_children_in_order() {
let document = html_tag().child(body()).child(head());
assert_eq!(document.render(), "<html><body></body><head></head></html>");
}
#[test]
fn an_optional_child_is_included_when_present() {
let present: Option<Element> = Some(Element::new("optional"));
let absent: Option<Element> = None;
assert_eq!(
html_tag().children_from(present).render(),
"<html><optional></optional></html>"
);
assert_eq!(html_tag().children_from(absent).render(), "<html></html>");
}
#[test]
fn a_single_child_gains_no_wrapper() {
let document = html_tag().child(body().child(h1().text("Hi")));
assert_eq!(document.render(), "<html><body><h1>Hi</h1></body></html>");
}
#[test]
fn an_if_else_renders_whichever_branch_holds() {
fn page(logged_in: bool) -> Element {
html_tag().child(html! {
@if logged_in { nav { "Sign out" } } @else { nav { "Sign in" } }
})
}
assert_eq!(page(true).render(), "<html><nav>Sign out</nav></html>");
assert_eq!(page(false).render(), "<html><nav>Sign in</nav></html>");
}
#[test]
fn a_sequence_of_children_flattens_into_siblings() {
let document = html_tag().children_from(["a", "b"].map(|text| p().text(text)));
assert_eq!(document.render(), "<html><p>a</p><p>b</p></html>");
}
#[test]
fn the_first_branch_of_a_condition_renders_alone() {
let document = html_tag().child(html! {
@if true { header } @else { footer }
});
assert_eq!(document.render(), "<html><header></header></html>");
}
#[test]
fn the_second_branch_of_a_condition_renders_alone() {
let document = html_tag().child(html! {
@if false { header } @else { footer }
});
assert_eq!(document.render(), "<html><footer></footer></html>");
}
}