Skip to main content

molt/
macros.rs

1//! The [`html!`](crate::html) declarative macro.
2//!
3//! Nested, markup-like syntax for building a tree. It is **sugar over the builder API, never a
4//! second code path** — every form here expands to the same `Element` and `Node` calls you
5//! would write by hand, so there is exactly one renderer to keep correct.
6
7use crate::core::{Element, Node};
8
9/// Builds a node tree from nested markup-like syntax.
10///
11/// # Syntax
12///
13/// | form | meaning |
14/// | --- | --- |
15/// | `div { … }` | an element with children |
16/// | `div(class = "card", id = "x") { … }` | attributes, then children |
17/// | `input(type = "email", required)` | a bare name is a boolean attribute |
18/// | `div("data-id" = 7, "hx-post" = "/a")` | a string key, for hyphenated names and keywords |
19/// | `"text"` | escaped text |
20/// | `(expr)` | an escaped `Display` expression |
21/// | `raw(expr)` | unescaped markup |
22/// | `@if cond { … } @else { … }` | conditional, empty branch renders to nothing |
23/// | `@for pat in iter { … }` | repetition |
24///
25/// # Examples
26///
27/// ```
28/// use molt::{html, prelude::*};
29///
30/// let page = html! {
31///     div(class = "card", id = "hero") {
32///         h1 { "Welcome" }
33///         p { "Fuel, tyres & chain." }
34///     }
35/// };
36///
37/// assert_eq!(
38///     page.render(),
39///     r#"<div class="card" id="hero"><h1>Welcome</h1><p>Fuel, tyres &amp; chain.</p></div>"#
40/// );
41/// ```
42///
43/// Conditionals and loops, matching `buildOptional` / `buildEither` / `buildArray`:
44///
45/// ```
46/// use molt::{html, prelude::*};
47///
48/// let names = ["Ana", "Bruno"];
49/// let logged_in = false;
50///
51/// let list = html! {
52///     ul {
53///         @for name in names { li { (name) } }
54///         @if logged_in { li { "Sign out" } }
55///     }
56/// };
57///
58/// assert_eq!(list.render(), "<ul><li>Ana</li><li>Bruno</li></ul>");
59/// ```
60///
61/// The expansion is the builder API, so the two are interchangeable:
62///
63/// ```
64/// use molt::{html, prelude::*};
65/// assert_eq!(
66///     html! { p(class = "lead") { "hi" } }.render(),
67///     Node::from(p().add_class("lead").child(Node::text("hi"))).render()
68/// );
69/// ```
70#[macro_export]
71macro_rules! html {
72    // A single root node.
73    ($($body:tt)*) => {{
74        let mut __nodes = $crate::macros::node_buffer();
75        $crate::html_nodes!(__nodes, $($body)*);
76        if __nodes.len() == 1 {
77            __nodes.pop().unwrap_or_else(|| $crate::Node::Fragment(::std::vec::Vec::new()))
78        } else {
79            $crate::Node::Fragment(__nodes)
80        }
81    }};
82}
83
84/// Accumulates sibling nodes into a `Vec`. Not part of the public API.
85#[doc(hidden)]
86#[macro_export]
87macro_rules! html_nodes {
88    // Terminal.
89    ($out:ident,) => {};
90
91    // @if and @for hand off to a token muncher. A `$cond:expr` fragment cannot be
92    // used before a brace: the expression parser would read `show { … }` as a struct
93    // literal and macro_rules cannot backtrack once a fragment has been consumed.
94    ($out:ident, @if $($tail:tt)*) => { $crate::html_if!($out, [] $($tail)*); };
95    ($out:ident, @for $($tail:tt)*) => { $crate::html_for!($out, [] $($tail)*); };
96
97    // raw(expr) — unescaped markup.
98    ($out:ident, raw($value:expr) $($rest:tt)*) => {
99        $out.push($crate::Node::raw(::std::string::ToString::to_string(&$value)));
100        $crate::html_nodes!($out, $($rest)*);
101    };
102
103    // Element with attributes and children.
104    ($out:ident, $tag:ident ( $($attrs:tt)* ) { $($children:tt)* } $($rest:tt)*) => {
105        {
106            let __el = $crate::elements::$tag();
107            let __el = $crate::html_attrs!(__el, $($attrs)*);
108            let mut __kids = $crate::macros::node_buffer();
109            $crate::html_nodes!(__kids, $($children)*);
110            $out.push($crate::Node::Element(
111                $crate::macros::element_with_children(__el, __kids)));
112        }
113        $crate::html_nodes!($out, $($rest)*);
114    };
115
116    // Element with attributes only.
117    ($out:ident, $tag:ident ( $($attrs:tt)* ) $($rest:tt)*) => {
118        {
119            let __el = $crate::elements::$tag();
120            $out.push($crate::Node::Element($crate::html_attrs!(__el, $($attrs)*)));
121        }
122        $crate::html_nodes!($out, $($rest)*);
123    };
124
125    // Element with children only.
126    ($out:ident, $tag:ident { $($children:tt)* } $($rest:tt)*) => {
127        {
128            let mut __kids = $crate::macros::node_buffer();
129            $crate::html_nodes!(__kids, $($children)*);
130            $out.push($crate::Node::Element(
131                $crate::macros::element_with_children($crate::elements::$tag(), __kids)));
132        }
133        $crate::html_nodes!($out, $($rest)*);
134    };
135
136    // Interpolated expression, escaped.
137    ($out:ident, ($value:expr) $($rest:tt)*) => {
138        $out.push($crate::macros::display_text(&$value));
139        $crate::html_nodes!($out, $($rest)*);
140    };
141
142    // Literal text, escaped.
143    ($out:ident, $text:literal $($rest:tt)*) => {
144        $out.push($crate::Node::text($text));
145        $crate::html_nodes!($out, $($rest)*);
146    };
147
148    // A bare element with no attributes and no children.
149    ($out:ident, $tag:ident $($rest:tt)*) => {
150        $out.push($crate::Node::Element($crate::elements::$tag()));
151        $crate::html_nodes!($out, $($rest)*);
152    };
153}
154
155/// Applies an attribute list to an element. Not part of the public API.
156#[doc(hidden)]
157#[macro_export]
158macro_rules! html_attrs {
159    ($el:expr,) => { $el };
160    ($el:expr) => { $el };
161
162    // "key" = value — a string key, for names that are Rust keywords such as `type`.
163    ($el:expr, $key:literal = $value:expr, $($rest:tt)*) => {
164        $crate::html_attrs!(
165            $crate::macros::attr_display($el, $key, &$value), $($rest)*)
166    };
167    ($el:expr, $key:literal = $value:expr) => {
168        $crate::macros::attr_display($el, $key, &$value)
169    };
170
171    // key = value
172    ($el:expr, $key:ident = $value:expr, $($rest:tt)*) => {
173        $crate::html_attrs!(
174            $crate::macros::attr_display($el, stringify!($key), &$value), $($rest)*)
175    };
176    ($el:expr, $key:ident = $value:expr) => {
177        $crate::macros::attr_display($el, stringify!($key), &$value)
178    };
179
180    // A bare name is a boolean attribute.
181    ($el:expr, $key:literal, $($rest:tt)*) => {
182        $crate::html_attrs!($el.bool_attr($key), $($rest)*)
183    };
184    ($el:expr, $key:literal) => {
185        $el.bool_attr($key)
186    };
187    ($el:expr, $key:ident, $($rest:tt)*) => {
188        $crate::html_attrs!($el.bool_attr(stringify!($key)), $($rest)*)
189    };
190    ($el:expr, $key:ident) => {
191        $el.bool_attr(stringify!($key))
192    };
193}
194
195/// A text node from a `Display` value, escaped as it is formatted. Not public API.
196#[doc(hidden)]
197#[must_use]
198pub fn display_text(value: &impl std::fmt::Display) -> Node {
199    Node::text_display(value)
200}
201
202/// Adds `key = value` to an element, formatting the value straight into escaped form. The
203/// key is always a literal in the macro, so it is borrowed, not allocated. Not public API.
204#[doc(hidden)]
205#[must_use]
206pub fn attr_display(
207    element: Element,
208    key: &'static str,
209    value: &impl std::fmt::Display,
210) -> Element {
211    element.add_attribute(crate::core::Attribute::from_display(key, value))
212}
213
214/// An empty sibling buffer for the macro to push into.
215///
216/// A function rather than an inline `Vec::new()` so that `clippy::vec_init_then_push` does
217/// not fire inside every `html!` expansion in every downstream crate. The lint is about
218/// hand-written style; this code is generated.
219#[doc(hidden)]
220#[must_use]
221pub fn node_buffer() -> Vec<Node> {
222    Vec::new()
223}
224
225/// Attaches macro-collected children to an element.
226///
227/// When every child is text, it becomes the element's **content** rather than a list of
228/// child nodes. That is what the builder API produces for `p().text("x")`, and the two
229/// render differently once pretty printing is on: content stays on the element's line,
230/// while a child text node gets its own indented line. Collapsing here is what keeps the
231/// macro true sugar over the builder rather than a second, subtly different code path.
232#[doc(hidden)]
233#[must_use]
234pub fn element_with_children(element: Element, children: Vec<Node>) -> Element {
235    let all_text = !children.is_empty() && children.iter().all(|c| matches!(c, Node::Text(_)));
236
237    if all_text {
238        // The first text node's string is reused as the buffer, so the common single-text
239        // child (`li { (name) }`) is moved, not copied.
240        let mut texts = children.into_iter().filter_map(|child| match child {
241            Node::Text(text) => Some(text.into_string()),
242            _ => None,
243        });
244        let mut content = texts.next().unwrap_or_default();
245        for text in texts {
246            content.push_str(&text);
247        }
248        // The text was escaped when each node was built, so it goes in verbatim.
249        return element.raw_text(content);
250    }
251
252    element.children_from(children)
253}
254
255/// Token muncher for `@if`. Collects condition tokens until the body block. Not public.
256#[doc(hidden)]
257#[macro_export]
258macro_rules! html_if {
259    ($out:ident, [$($cond:tt)+] { $($body:tt)* } @else { $($alt:tt)* } $($rest:tt)*) => {
260        if $($cond)+ { $crate::html_nodes!($out, $($body)*); }
261        else { $crate::html_nodes!($out, $($alt)*); }
262        $crate::html_nodes!($out, $($rest)*);
263    };
264    ($out:ident, [$($cond:tt)+] { $($body:tt)* } $($rest:tt)*) => {
265        if $($cond)+ { $crate::html_nodes!($out, $($body)*); }
266        $crate::html_nodes!($out, $($rest)*);
267    };
268    ($out:ident, [$($cond:tt)*] $next:tt $($rest:tt)*) => {
269        $crate::html_if!($out, [$($cond)* $next] $($rest)*);
270    };
271}
272
273/// Token muncher for `@for`. Collects `pat in iter` until the body block. Not public.
274#[doc(hidden)]
275#[macro_export]
276macro_rules! html_for {
277    ($out:ident, [$($head:tt)+] { $($body:tt)* } $($rest:tt)*) => {
278        for $($head)+ { $crate::html_nodes!($out, $($body)*); }
279        $crate::html_nodes!($out, $($rest)*);
280    };
281    ($out:ident, [$($head:tt)*] $next:tt $($rest:tt)*) => {
282        $crate::html_for!($out, [$($head)* $next] $($rest)*);
283    };
284}
285
286#[cfg(test)]
287mod tests {
288    /// A `Display` impl that writes in several pieces, each holding a special character.
289    /// Escaping per piece must equal escaping the whole.
290    struct Pieces;
291
292    impl std::fmt::Display for Pieces {
293        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294            f.write_str("a<")?;
295            f.write_str("b&")?;
296            write!(f, "{}\"c'", 7)
297        }
298    }
299
300    #[test]
301    fn a_display_value_is_escaped_as_it_is_formatted() {
302        let text = crate::html! { p { (Pieces) } };
303        assert_eq!(text.render(), "<p>a&lt;b&amp;7&quot;c&#x27;</p>");
304        let attribute = crate::html! { p(title = Pieces) { "x" } };
305        assert_eq!(
306            attribute.render(),
307            "<p title=\"a<b&amp;7&quot;c&#x27;\">x</p>"
308        );
309        assert_eq!(
310            text.render(),
311            Node::from(crate::elements::p().text(Pieces.to_string())).render()
312        );
313    }
314
315    #[test]
316    fn numbers_and_keyword_keys_still_work_as_attributes() {
317        let image = crate::html! { img(width = 100, "type" = 'x', hidden) };
318        assert_eq!(image.render(), r#"<img width="100" type="x" hidden>"#);
319    }
320
321    /// `data-*`, `aria-*` and `hx-*` are not Rust identifiers, so they take the string-key
322    /// form. It must produce the same tree the builder's `data_attr`/`aria_attr`/`attr` do.
323    #[test]
324    fn hyphenated_attribute_names_use_the_string_key_form() {
325        let id = 7;
326        let el = crate::html! {
327            button("data-id" = id, "aria-label" = "Save", "hx-post" = "/save", "hx-swap") {
328                "Save"
329            }
330        };
331        let expected =
332            r#"<button data-id="7" aria-label="Save" hx-post="/save" hx-swap>Save</button>"#;
333        assert_eq!(el.render(), expected);
334        let built = crate::elements::button()
335            .data_attr("id", "7")
336            .aria_attr("label", "Save")
337            .attr("hx-post", "/save")
338            .bool_attr("hx-swap")
339            .text("Save");
340        assert_eq!(el.render(), Node::from(built).render());
341    }
342
343    #[test]
344    fn several_text_children_are_joined_into_one_content_string() {
345        let paragraph = crate::html! { p { "a " (1) " <b>" } };
346        assert_eq!(paragraph.render(), "<p>a 1 &lt;b&gt;</p>");
347    }
348
349    use crate::prelude::*;
350
351    #[test]
352    fn a_bare_element_renders_empty() {
353        assert_eq!(html! { div }.render(), "<div></div>");
354    }
355
356    #[test]
357    fn text_children_are_escaped() {
358        assert_eq!(html! { p { "a & b" } }.render(), "<p>a &amp; b</p>");
359    }
360
361    #[test]
362    fn raw_children_are_not_escaped() {
363        assert_eq!(html! { p { raw("<b>x</b>") } }.render(), "<p><b>x</b></p>");
364    }
365
366    #[test]
367    fn attributes_and_children_combine() {
368        assert_eq!(
369            html! { div(class = "card", id = "hero") { p { "hi" } } }.render(),
370            r#"<div class="card" id="hero"><p>hi</p></div>"#
371        );
372    }
373
374    #[test]
375    fn a_bare_attribute_name_is_a_boolean_attribute() {
376        assert_eq!(
377            html! { input("type" = "email", name = "email", required) }.render(),
378            r#"<input type="email" name="email" required>"#
379        );
380    }
381
382    #[test]
383    fn expressions_interpolate_escaped() {
384        let name = "Tom & Jerry";
385        assert_eq!(
386            html! { span { (name) } }.render(),
387            "<span>Tom &amp; Jerry</span>"
388        );
389    }
390
391    /// A `@for` produces one sibling per item.
392    #[test]
393    fn for_loops_expand_to_siblings() {
394        let names = ["Ana", "Bruno"];
395        assert_eq!(
396            html! { ul { @for n in names { li { (n) } } } }.render(),
397            "<ul><li>Ana</li><li>Bruno</li></ul>"
398        );
399    }
400
401    /// Matches `buildOptional`: a false branch renders to nothing at all.
402    #[test]
403    fn a_false_condition_contributes_no_node() {
404        let show = false;
405        assert_eq!(
406            html! { div { @if show { p { "x" } } } }.render(),
407            "<div></div>"
408        );
409    }
410
411    /// Matches `buildEither`.
412    #[test]
413    fn if_else_picks_one_branch() {
414        let logged_in = true;
415        let markup = html! {
416            nav { @if logged_in { a { "Sign out" } } @else { a { "Sign in" } } }
417        };
418        assert_eq!(markup.render(), "<nav><a>Sign out</a></nav>");
419    }
420
421    #[test]
422    fn several_roots_become_a_fragment() {
423        assert_eq!(html! { p { "a" } p { "b" } }.render(), "<p>a</p><p>b</p>");
424    }
425
426    /// The macro is sugar: it must produce exactly what the builder produces.
427    #[test]
428    fn macro_output_equals_the_builder_output() {
429        let from_macro = html! { div(class = "card") { h1 { "T" } p { "B" } } };
430        let from_builder = Node::from(
431            div()
432                .add_class("card")
433                .child(h1().text("T"))
434                .child(p().text("B")),
435        );
436        assert_eq!(from_macro.render(), from_builder.render());
437        assert_eq!(from_macro.render_pretty(), from_builder.render_pretty());
438    }
439
440    #[test]
441    fn nesting_survives_pretty_printing() {
442        let markup = html! { div { section { p { "deep" } } } };
443        assert_eq!(
444            markup.render_pretty(),
445            "<div>\n  <section>\n    <p>deep</p>\n  </section>\n</div>"
446        );
447    }
448
449    // `html!` promises to be exactly equivalent to the builder — rule 9 of AGENTS.md: sugar over
450    // the builder, never a second code path — so each of these asserts the macro and the
451    // builder agree, and then pins the markup.
452
453    #[test]
454    fn the_macro_and_the_builder_agree_on_a_plain_container() {
455        let macro_built = html! { div { p { "Hi" } } };
456        let builder_built = div().child(p().text("Hi"));
457
458        assert_eq!(macro_built.render(), builder_built.render());
459        assert_eq!(macro_built.render(), "<div><p>Hi</p></div>");
460    }
461
462    #[test]
463    fn the_macro_and_the_builder_agree_on_a_required_attribute() {
464        let macro_built = html! { a(href = "/docs") { span { "Docs" } } };
465        let builder_built = link_to("/docs").child(span().text("Docs"));
466
467        assert_eq!(macro_built.render(), builder_built.render());
468        assert_eq!(
469            macro_built.render(),
470            r#"<a href="/docs"><span>Docs</span></a>"#
471        );
472    }
473
474    #[test]
475    fn attributes_and_children_render_together() {
476        assert_eq!(
477            html! { div(id = "main") { p { "Hi" } } }.render(),
478            r#"<div id="main"><p>Hi</p></div>"#
479        );
480    }
481
482    #[test]
483    fn a_boolean_flag_survives_the_macro() {
484        assert_eq!(
485            html! { details(open) { summary { "More" } } }.render(),
486            "<details open><summary>More</summary></details>"
487        );
488    }
489
490    #[test]
491    fn the_table_family_nests() {
492        let markup = html! {
493            table {
494                thead { tr { th { "A" } } }
495                tbody { tr { td { "1" } } }
496            }
497        };
498
499        assert_eq!(
500            markup.render(),
501            "<table><thead><tr><th>A</th></tr></thead><tbody><tr><td>1</td></tr></tbody></table>"
502        );
503    }
504
505    /// `for` is a Rust keyword, so the attribute name is quoted — the one place the macro asks
506    /// for punctuation.
507    #[test]
508    fn the_form_family_nests() {
509        let markup = html! {
510            form {
511                fieldset {
512                    legend { "Account" }
513                    label("for" = "email") { "Email" }
514                }
515            }
516        };
517
518        assert_eq!(
519            markup.render(),
520            concat!(
521                "<form><fieldset><legend>Account</legend>",
522                r#"<label for="email">Email</label></fieldset></form>"#,
523            )
524        );
525    }
526
527    #[test]
528    fn the_media_family_nests() {
529        let markup = html! {
530            picture {
531                source(srcset = "a.webp", "type" = "image/webp")
532                img(src = "a.jpg", alt = "A")
533            }
534        };
535
536        assert_eq!(
537            markup.render(),
538            concat!(
539                r#"<picture><source srcset="a.webp" type="image/webp">"#,
540                r#"<img src="a.jpg" alt="A"></picture>"#,
541            )
542        );
543    }
544
545    #[test]
546    fn a_loop_inside_the_macro_repeats_its_body() {
547        let markup = html! { ul { @for name in ["a", "b", "c"] { li { (name) } } } };
548
549        assert_eq!(markup.render(), "<ul><li>a</li><li>b</li><li>c</li></ul>");
550    }
551
552    #[test]
553    fn a_false_condition_inside_the_macro_renders_nothing() {
554        let is_admin = false;
555        let markup = html! {
556            nav {
557                a(href = "/") { "Home" }
558                @if is_admin { a(href = "/admin") { "Admin" } }
559            }
560        };
561
562        assert_eq!(markup.render(), r#"<nav><a href="/">Home</a></nav>"#);
563    }
564
565    /// The macro takes an iterator through `@for`, and `children_from` is the builder's
566    /// spelling of the same thing — the test is that both land on identical markup.
567    #[test]
568    fn a_mapped_sequence_matches_the_macro_loop() {
569        let macro_built = html! { ol { @for item in ["x", "y"] { li { (item) } } } };
570        let builder_built = ol().children_from(["x", "y"].map(|item| li().text(item)));
571
572        assert_eq!(macro_built.render(), builder_built.render());
573        assert_eq!(macro_built.render(), "<ol><li>x</li><li>y</li></ol>");
574    }
575
576    #[test]
577    fn an_empty_body_produces_an_empty_element() {
578        assert_eq!(html! { div {} }.render(), "<div></div>");
579    }
580
581    /// The macro resolves a tag name to its generated constructor, and there is no
582    /// `hgroup()` among the 93. `Element::new` is the escape hatch for a tag without a
583    /// constructor, and it composes with macro-built children.
584    #[test]
585    fn an_untyped_element_takes_macro_built_children() {
586        let group = Element::new("hgroup")
587            .child(html! { h1 { "Title" } })
588            .child(html! { p { "Subtitle" } });
589
590        assert_eq!(
591            group.render(),
592            "<hgroup><h1>Title</h1><p>Subtitle</p></hgroup>"
593        );
594    }
595
596    /// `html!` returns a [`Node`], which is the union of every shape a child can take
597    /// and has no builder methods — so chaining happens on the builder, and the macro
598    /// supplies the children. Both spellings render the same.
599    #[test]
600    fn chaining_happens_on_the_builder_side() {
601        let chained = div().child(html! { p { "Hi" } }).add_class("card");
602
603        assert_eq!(chained.render(), r#"<div class="card"><p>Hi</p></div>"#);
604    }
605
606    #[test]
607    fn nesting_goes_as_deep_as_it_is_written() {
608        let page = html! {
609            body { main_tag { section { article { h2 { "Title" } } } } }
610        };
611
612        assert_eq!(
613            page.render(),
614            "<body><main><section><article><h2>Title</h2></article></section></main></body>"
615        );
616    }
617
618    // `html!` lowers `@if`/`@else`/`@for` straight onto the builder. These tests assert markup
619    // rather than reaching into the tree, which is rule 11 of AGENTS.md and what lets the
620    // internals be rewritten without touching them.
621
622    #[test]
623    fn a_root_html_element_keeps_its_children_in_order() {
624        let document = html_tag().child(body()).child(head());
625
626        assert_eq!(document.render(), "<html><body></body><head></head></html>");
627    }
628
629    /// `Option` is an iterator of zero or one item, so `children_from` is all the plumbing
630    /// an optional child needs.
631    #[test]
632    fn an_optional_child_is_included_when_present() {
633        let present: Option<Element> = Some(Element::new("optional"));
634        let absent: Option<Element> = None;
635
636        assert_eq!(
637            html_tag().children_from(present).render(),
638            "<html><optional></optional></html>"
639        );
640        assert_eq!(html_tag().children_from(absent).render(), "<html></html>");
641    }
642
643    #[test]
644    fn a_single_child_gains_no_wrapper() {
645        let document = html_tag().child(body().child(h1().text("Hi")));
646
647        assert_eq!(document.render(), "<html><body><h1>Hi</h1></body></html>");
648    }
649
650    #[test]
651    fn an_if_else_renders_whichever_branch_holds() {
652        fn page(logged_in: bool) -> Element {
653            html_tag().child(html! {
654                @if logged_in { nav { "Sign out" } } @else { nav { "Sign in" } }
655            })
656        }
657
658        assert_eq!(page(true).render(), "<html><nav>Sign out</nav></html>");
659        assert_eq!(page(false).render(), "<html><nav>Sign in</nav></html>");
660    }
661
662    #[test]
663    fn a_sequence_of_children_flattens_into_siblings() {
664        let document = html_tag().children_from(["a", "b"].map(|text| p().text(text)));
665
666        assert_eq!(document.render(), "<html><p>a</p><p>b</p></html>");
667    }
668
669    /// The macro resolves a tag name to its generated constructor, so real tags stand in.
670    #[test]
671    fn the_first_branch_of_a_condition_renders_alone() {
672        let document = html_tag().child(html! {
673            @if true { header } @else { footer }
674        });
675
676        assert_eq!(document.render(), "<html><header></header></html>");
677    }
678
679    #[test]
680    fn the_second_branch_of_a_condition_renders_alone() {
681        let document = html_tag().child(html! {
682            @if false { header } @else { footer }
683        });
684
685        assert_eq!(document.render(), "<html><footer></footer></html>");
686    }
687}