Skip to main content

molt/core/
node.rs

1//! The node tree.
2//!
3//! Elements, text, raw markup, comments and fragments are the variants of one **closed enum**,
4//! with a single `match` in the writer. A closed enum rather than trait objects means the
5//! tree is `Send + Sync`, `Clone`, `PartialEq` and `Hash` without ceremony, and that is what
6//! makes the `parallel` feature sound.
7
8use crate::core::attribute::Attribute;
9use crate::core::element::Element;
10use crate::core::escape::write_escaped_text;
11use crate::core::render::{Render, RenderOptions};
12use crate::core::tags::{is_raw_text, is_valid_tag_name, is_void, is_whitespace_sensitive};
13
14/// A node in the HTML tree.
15///
16/// # Examples
17/// ```
18/// use molt::prelude::*;
19/// let node = Node::from(div().text("hi"));
20/// assert_eq!(node.render(), "<div>hi</div>");
21/// ```
22#[derive(Debug)]
23#[non_exhaustive]
24pub enum Node {
25    /// An element with a tag name, attributes and children.
26    Element(Element),
27    /// Escaped text. The escaping already happened — see [`crate::core::escape`].
28    Text(Markup),
29    /// Verbatim markup, never escaped. For imported snippets: an SVG, an embed code.
30    ///
31    /// Unlike [`Node::Fragment`], this is an opaque string, so pretty printing can only
32    /// indent the whole blob. Reach for a fragment when you want the children indented.
33    Raw(Markup),
34    /// An HTML comment.
35    ///
36    /// The content has `--` neutralised so the comment cannot be closed early.
37    Comment(Markup),
38    /// A transparent group that renders its children with no wrapper element.
39    ///
40    /// Use it to return several nodes from one expression — a `map` of cards, the body of
41    /// an `if`, a group of `<meta>` tags — without introducing an extra `<div>`.
42    Fragment(Vec<Node>),
43}
44
45/// A string that is safe to write into the output exactly as it is.
46///
47/// What a [`Node::Text`], [`Node::Raw`] or [`Node::Comment`] holds. It has **no public
48/// constructor**: the only ways in are [`Node::text`] (which escapes), [`Node::comment`]
49/// (which neutralises `--`) and [`Node::raw`] (the greppable escape hatch). Before this
50/// type the variants held a bare `String`, so `Node::Text("<script>".into())` skipped the
51/// escaping that the rest of the crate promises happens once, on entry.
52///
53/// # Examples
54/// ```
55/// use molt::prelude::*;
56/// let Node::Text(text) = Node::text("a < b") else { unreachable!() };
57/// assert_eq!(text.as_str(), "a &lt; b");
58/// ```
59///
60/// Building one directly does not compile, which is the point:
61///
62/// ```compile_fail
63/// use molt::core::{Markup, Node};
64/// let _ = Node::Text(Markup("<script>".into()));
65/// ```
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub struct Markup(String);
68
69impl Markup {
70    /// Takes the string out, so a caller that owns the node does not copy it.
71    pub(crate) fn into_string(self) -> String {
72        self.0
73    }
74
75    /// The content, already escaped or neutralised as its node requires.
76    #[must_use]
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80}
81
82impl std::ops::Deref for Markup {
83    type Target = str;
84
85    fn deref(&self) -> &str {
86        &self.0
87    }
88}
89
90impl AsRef<str> for Markup {
91    fn as_ref(&self) -> &str {
92        &self.0
93    }
94}
95
96impl std::fmt::Display for Markup {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str(&self.0)
99    }
100}
101
102impl PartialEq<str> for Markup {
103    fn eq(&self, other: &str) -> bool {
104        self.0 == other
105    }
106}
107
108impl PartialEq<&str> for Markup {
109    fn eq(&self, other: &&str) -> bool {
110        self.0 == *other
111    }
112}
113
114impl Node {
115    /// Creates a text node, escaping the content.
116    #[must_use]
117    pub fn text(content: impl AsRef<str>) -> Self {
118        let raw = content.as_ref();
119        let mut escaped = String::with_capacity(raw.len());
120        write_escaped_text(&mut escaped, raw, false);
121        Self::Text(Markup(escaped))
122    }
123
124    /// A text node formatted from a `Display` value directly into its escaped form, with no
125    /// intermediate `String`. What `html!` uses for `(expr)`.
126    pub(crate) fn text_display(value: &impl std::fmt::Display) -> Self {
127        use std::fmt::Write as _;
128        let mut escaped = String::new();
129        // Writing into a `String` cannot fail; a `Display` impl that errors leaves a short value.
130        let _ = write!(crate::core::escape::TextWriter(&mut escaped), "{value}");
131        Self::Text(Markup(escaped))
132    }
133
134    /// Creates a raw markup node. The content is **not** escaped.
135    ///
136    /// One of the three escape hatches, with [`Element::raw_text`](crate::core::Element::raw_text)
137    /// and [`Attribute::raw`](crate::core::Attribute::raw); grep for the three to audit a
138    /// codebase. Use it for markup you wrote or already sanitised: an SVG icon, an embed
139    /// snippet. **Never pass text a user typed.** `<` in it becomes markup, `</script>` ends a
140    /// script, and nothing downstream will stop it.
141    ///
142    /// For JSON in a `<script>` use [`seo::json_ld`](crate::seo::json_ld), which escapes it.
143    /// To show untrusted text, use [`Node::text`].
144    ///
145    /// # Examples
146    /// ```
147    /// use molt::prelude::*;
148    /// let icon = Node::raw(r#"<svg viewBox="0 0 1 1"><path d="M0 0h1v1z"/></svg>"#);
149    /// assert!(icon.render().starts_with("<svg"));
150    ///
151    /// // The same text through `Node::text` is shown, not interpreted.
152    /// assert_eq!(Node::text("<b>").render(), "&lt;b&gt;");
153    /// ```
154    #[must_use]
155    pub fn raw(content: impl Into<String>) -> Self {
156        Self::Raw(Markup(content.into()))
157    }
158
159    /// Creates a comment node.
160    ///
161    /// A space is put between every two adjacent `-` and after a trailing `-`, so no run of
162    /// two hyphens survives and the comment cannot terminate early and inject markup.
163    ///
164    /// # Examples
165    /// ```
166    /// use molt::prelude::*;
167    /// assert_eq!(Node::comment("a -- b").render(), "<!-- a - - b -->");
168    /// assert_eq!(Node::comment("x--->").render(), "<!-- x- - -> -->");
169    /// ```
170    #[must_use]
171    pub fn comment(content: impl AsRef<str>) -> Self {
172        let mut neutralised = String::with_capacity(content.as_ref().len());
173        push_neutralised_comment(&mut neutralised, content.as_ref());
174        Self::Comment(Markup(neutralised))
175    }
176
177    /// Creates a transparent group of nodes.
178    #[must_use]
179    pub fn fragment(children: impl IntoIterator<Item = Node>) -> Self {
180        Self::Fragment(children.into_iter().collect())
181    }
182
183    /// Whether this node renders to nothing.
184    ///
185    /// The pretty-print writer arrives at the same answer without calling this: it lets a
186    /// child write straight into the output and rolls the separator back if the child wrote
187    /// nothing, which is what stops an empty fragment — the body of a false `if` — from
188    /// leaving a blank line behind.
189    #[must_use]
190    pub fn is_empty(&self) -> bool {
191        match self {
192            Self::Element(_) | Self::Comment(_) => false,
193            Self::Text(s) | Self::Raw(s) => s.is_empty(),
194            Self::Fragment(children) => {
195                // Iterative: a chain of nested fragments, which nothing else bounds, must
196                // not cost a stack frame per level.
197                let mut pending: Vec<&Node> = children.iter().collect();
198                while let Some(node) = pending.pop() {
199                    match node {
200                        Self::Element(_) | Self::Comment(_) => return false,
201                        Self::Text(s) | Self::Raw(s) => {
202                            if !s.is_empty() {
203                                return false;
204                            }
205                        }
206                        Self::Fragment(children) => pending.extend(children),
207                    }
208                }
209                true
210            }
211        }
212    }
213}
214
215// `Clone` and `PartialEq` are written by hand, not derived: the derives recurse once per
216// level and a stack overflow is an abort, so comparing or copying a tree the renderer
217// handles fine would have killed the process. `Debug` is still derived — formatting a
218// tree that deep is a diagnostic nobody reads, and `{:?}` on one is documented as unsafe
219// in `SECURITY.md`.
220impl Clone for Node {
221    fn clone(&self) -> Self {
222        crate::core::element::clone_tree(Source::Node(self))
223    }
224}
225
226impl PartialEq for Node {
227    fn eq(&self, other: &Self) -> bool {
228        crate::core::element::trees_equal(Source::Node(self), Source::Node(other))
229    }
230}
231
232impl Eq for Node {}
233
234/// Where an iterative tree walk starts: a node, or an element that has no `Node` around it.
235#[derive(Clone, Copy)]
236pub(crate) enum Source<'a> {
237    Node(&'a Node),
238    Element(&'a Element),
239}
240
241impl std::fmt::Display for Node {
242    /// The compact render, or the pretty one with the alternate flag: `{node}` / `{node:#}`.
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.write_str(&if f.alternate() {
245            self.render_pretty()
246        } else {
247            self.render()
248        })
249    }
250}
251
252impl std::hash::Hash for Node {
253    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
254        crate::core::element::hash_tree(Source::Node(self), state);
255    }
256}
257
258/// Collects nodes into a [`Node::Fragment`], so `.collect::<Node>()` replaces
259/// `Node::fragment(iter)`.
260impl<N: Into<Node>> FromIterator<N> for Node {
261    fn from_iter<I: IntoIterator<Item = N>>(iter: I) -> Self {
262        Self::Fragment(iter.into_iter().map(Into::into).collect())
263    }
264}
265
266impl From<Vec<Node>> for Node {
267    fn from(children: Vec<Node>) -> Self {
268        Self::Fragment(children)
269    }
270}
271
272/// `None` is an empty fragment, which renders to nothing and leaves no blank line.
273impl<T: Into<Node>> From<Option<T>> for Node {
274    fn from(value: Option<T>) -> Self {
275        value.map_or_else(|| Self::Fragment(Vec::new()), Into::into)
276    }
277}
278
279impl From<char> for Node {
280    fn from(c: char) -> Self {
281        Self::text(c.to_string())
282    }
283}
284
285impl From<Element> for Node {
286    fn from(element: Element) -> Self {
287        Self::Element(element)
288    }
289}
290
291impl From<&str> for Node {
292    fn from(text: &str) -> Self {
293        Self::text(text)
294    }
295}
296
297impl From<String> for Node {
298    fn from(text: String) -> Self {
299        Self::text(text)
300    }
301}
302
303impl Render for Node {
304    fn write_into(&self, out: &mut String, options: &RenderOptions, depth: usize) {
305        write_tree(
306            if options.pretty {
307                Step::Pretty(self, depth)
308            } else {
309                Step::Compact(self)
310            },
311            out,
312            options,
313        );
314    }
315}
316
317/// Renders an element without wrapping it in a [`Node`] first.
318///
319/// [`Render for Element`](Element) used to do `Node::Element(self.clone()).write_into(..)`,
320/// which deep-copied the whole subtree on every render — and cloning is itself recursive,
321/// so a deep tree aborted in `Clone` before the writer ever saw it.
322pub(crate) fn write_element_tree(
323    element: &Element,
324    out: &mut String,
325    options: &RenderOptions,
326    depth: usize,
327) {
328    write_tree(
329        if options.pretty {
330            Step::PrettyElement(element, depth)
331        } else {
332            Step::CompactElement(element)
333        },
334        out,
335        options,
336    );
337}
338
339/// Writes `<html>` around a borrowed `head` and `body`, without copying either.
340///
341/// `Document::root` builds the same tree by cloning both, and cloning is recursive, so
342/// rendering a document with a deep body aborted in `Clone` before the writer ran. The
343/// bytes are identical to rendering that tree: this queues the same steps
344/// `push_compact_element`/`push_pretty_element` would for an `<html>` with two children.
345pub(crate) fn write_document_root(
346    lang: Option<&str>,
347    head: &Element,
348    body: &Element,
349    out: &mut String,
350    options: &RenderOptions,
351) {
352    if options.pretty {
353        options.write_indent(out, 0);
354    }
355    out.push_str("<html");
356    if let Some(lang) = lang {
357        Attribute::new_static("lang", lang).write_into(out);
358    }
359    out.push('>');
360
361    // The stack is a LIFO: the last step pushed runs first.
362    let steps = if options.pretty {
363        vec![
364            Step::ClosePretty("html", 0),
365            Step::PrettyChildElement {
366                element: body,
367                depth: 1,
368            },
369            Step::PrettyChildElement {
370                element: head,
371                depth: 1,
372            },
373        ]
374    } else {
375        vec![
376            Step::Close("html"),
377            Step::CompactElement(body),
378            Step::CompactElement(head),
379        ]
380    };
381    // `<html>` is written above by hand, so head and body already sit one level down.
382    run_steps(steps, out, options, 1);
383}
384
385/// One unit of work for the writer.
386///
387/// The writer used to recurse once per nesting level, which made tree depth a stack
388/// limit: roughly 2,000 levels aborted the process, and a stack overflow is an abort, not
389/// a panic anything can catch. Each variant here is what one of those recursive calls
390/// used to do, held on an explicit stack instead — so depth costs heap, bounded by a tree
391/// that is already in memory.
392enum Step<'a> {
393    /// Write this node on one line.
394    Compact(&'a Node),
395    /// Write this node indented to `depth`.
396    Pretty(&'a Node, usize),
397    /// Write this element on one line.
398    CompactElement(&'a Element),
399    /// Write this element indented to `depth`.
400    PrettyElement(&'a Element, usize),
401    /// `</tag>`.
402    Close(&'a str),
403    /// A newline, indentation to `depth`, then `</tag>`.
404    ClosePretty(&'a str, usize),
405    /// A child in pretty mode: writes the separating newline, renders the child, and then
406    /// takes both back out if the child rendered to nothing.
407    ///
408    /// `group_start` is `Some` inside a fragment, where the separator is only written once
409    /// something has already been emitted, and `None` inside an element, where every child
410    /// gets one.
411    PrettyChild {
412        node: &'a Node,
413        depth: usize,
414        group_start: Option<usize>,
415    },
416    /// An element child of a borrowed parent, in pretty mode: [`Step::PrettyChild`] for
417    /// callers that hold `&Element` and have no `Node` to point at.
418    PrettyChildElement { element: &'a Element, depth: usize },
419    /// Truncates back to `mark` when nothing was appended after `after`.
420    ///
421    /// This replaces rendering each child into a scratch buffer to test it for emptiness:
422    /// the child writes straight into the output and its separator is rolled back if it
423    /// wrote nothing, which is what still keeps an empty fragment — the body of a false
424    /// `@if` — from leaving a blank line behind.
425    DropIfEmpty { mark: usize, after: usize },
426}
427
428/// Drives the steps until the tree is written.
429fn write_tree(start: Step<'_>, out: &mut String, options: &RenderOptions) {
430    let mut stack = Vec::with_capacity(16);
431    stack.push(start);
432    run_steps(stack, out, options, 0);
433}
434
435/// Runs a prepared stack to completion.
436fn run_steps(stack: Vec<Step<'_>>, out: &mut String, options: &RenderOptions, already_open: usize) {
437    // Two monomorphised copies: the one that counts open elements for `max_depth`, and the
438    // one that does not. The counter costs a few percent per element on a wide tree, and most
439    // renders set no limit, so they run the copy without it.
440    if options.max_depth.is_some() {
441        run_steps_with::<true>(stack, out, options, already_open);
442    } else {
443        run_steps_with::<false>(stack, out, options, already_open);
444    }
445}
446
447// One `match` over every `Step`, which is the writer's whole state machine: splitting it
448// would hide the single place where all the cases can be read together.
449#[allow(clippy::too_many_lines)]
450fn run_steps_with<const LIMITED: bool>(
451    mut stack: Vec<Step<'_>>,
452    out: &mut String,
453    options: &RenderOptions,
454    already_open: usize,
455) {
456    // How many raw-text elements (`noscript`, `script`, …) enclose the node being written.
457    // Their descendants' attribute values get `<` and `>` escaped; see
458    // `Attribute::write_into_in`.
459    let mut raw_text_depth = 0usize;
460    // How many elements are open around the node being written, for `max_depth`. Counted
461    // here, not read from the pretty indent depth, so compact mode is covered too.
462    let mut open_depth = already_open;
463    let max_depth = options.max_depth;
464    while let Some(step) = stack.pop() {
465        match step {
466            // Escaped text and verbatim markup write the same way; they differ only in
467            // when the escaping happened, which is at construction.
468            Step::Compact(Node::Text(text) | Node::Raw(text)) => out.push_str(text),
469            Step::Compact(Node::Comment(content)) => write_comment(out, content),
470            Step::Compact(Node::Fragment(children)) => {
471                stack.extend(children.iter().rev().map(Step::Compact));
472            }
473            Step::Compact(Node::Element(element)) => stack.push(Step::CompactElement(element)),
474
475            Step::Pretty(Node::Text(text) | Node::Raw(text), depth) => {
476                options.write_indent(out, depth);
477                out.push_str(text);
478            }
479            Step::Pretty(Node::Comment(content), depth) => {
480                options.write_indent(out, depth);
481                write_comment(out, content);
482            }
483            Step::Pretty(Node::Fragment(children), depth) => {
484                // The children sit at the *parent's* depth, joined by newlines, with empty
485                // ones skipped entirely.
486                let group_start = out.len();
487                stack.extend(children.iter().rev().map(|child| Step::PrettyChild {
488                    node: child,
489                    depth,
490                    group_start: Some(group_start),
491                }));
492            }
493            Step::Pretty(Node::Element(element), depth) => {
494                stack.push(Step::PrettyElement(element, depth));
495            }
496
497            Step::CompactElement(element) => {
498                // Too deep: write nothing, before any indent, so pretty mode's rollback of
499                // the child separator leaves no blank line (the path an invalid tag takes).
500                if LIMITED && max_depth.is_some_and(|max| open_depth >= max) {
501                    continue;
502                }
503                let in_raw_text = raw_text_depth > 0;
504                if push_compact_element(element, out, options, &mut stack, in_raw_text) {
505                    if LIMITED {
506                        open_depth += 1;
507                    }
508                    if is_raw_text(element.tag()) {
509                        raw_text_depth += 1;
510                    }
511                }
512            }
513            Step::PrettyElement(element, depth) => {
514                if LIMITED && max_depth.is_some_and(|max| open_depth >= max) {
515                    continue;
516                }
517                let in_raw_text = raw_text_depth > 0;
518                if push_pretty_element(element, depth, out, options, &mut stack, in_raw_text) {
519                    if LIMITED {
520                        open_depth += 1;
521                    }
522                    if is_raw_text(element.tag()) {
523                        raw_text_depth += 1;
524                    }
525                }
526            }
527
528            Step::Close(tag) => {
529                if LIMITED {
530                    open_depth = open_depth.saturating_sub(1);
531                }
532                if is_raw_text(tag) {
533                    raw_text_depth = raw_text_depth.saturating_sub(1);
534                }
535                out.push_str("</");
536                out.push_str(tag);
537                out.push('>');
538            }
539            Step::ClosePretty(tag, depth) => {
540                if LIMITED {
541                    open_depth = open_depth.saturating_sub(1);
542                }
543                if is_raw_text(tag) {
544                    raw_text_depth = raw_text_depth.saturating_sub(1);
545                }
546                out.push('\n');
547                options.write_indent(out, depth);
548                out.push_str("</");
549                out.push_str(tag);
550                out.push('>');
551            }
552
553            Step::PrettyChild {
554                node,
555                depth,
556                group_start,
557            } => {
558                let mark = out.len();
559                let needs_separator = group_start.is_none_or(|start| out.len() > start);
560                if needs_separator {
561                    out.push('\n');
562                }
563                let after = out.len();
564                // Pushed first so it runs last: the child goes on top of it.
565                stack.push(Step::DropIfEmpty { mark, after });
566                stack.push(Step::Pretty(node, depth));
567            }
568            Step::PrettyChildElement { element, depth } => {
569                let mark = out.len();
570                out.push('\n');
571                let after = out.len();
572                stack.push(Step::DropIfEmpty { mark, after });
573                stack.push(Step::PrettyElement(element, depth));
574            }
575            Step::DropIfEmpty { mark, after } => {
576                if out.len() == after {
577                    out.truncate(mark);
578                }
579            }
580        }
581    }
582}
583
584/// Writes an element's opening token and queues everything that follows it.
585///
586/// Returns whether a closing step was queued, i.e. whether the element is now open.
587fn push_compact_element<'a>(
588    element: &'a Element,
589    out: &mut String,
590    options: &RenderOptions,
591    stack: &mut Vec<Step<'a>>,
592    in_raw_text: bool,
593) -> bool {
594    // An unsafe tag name is dropped with its whole subtree rather than repaired: there is
595    // no right guess for what `div><script>` was meant to be.
596    if !is_valid_tag_name(element.tag()) {
597        return false;
598    }
599    out.push('<');
600    out.push_str(element.tag());
601    write_attributes(element.attributes(), out, in_raw_text, element.tag());
602
603    // A void element takes no content and no children — both are silently dropped; that is
604    // the contract. `Element::self_closing` opts a non-void tag into the same
605    // shape, so a caller can emit `<input />` without building a `Node::Raw` that would
606    // skip the URL policy in `Attribute::write_into`.
607    if is_void(element.tag()) || element.is_self_closing() {
608        write_void_suffix(out, options);
609        return false;
610    }
611
612    out.push('>');
613    if let Some(content) = element.content() {
614        out.push_str(content);
615    }
616    stack.push(Step::Close(element.tag()));
617    stack.extend(element.children().iter().rev().map(Step::Compact));
618    true
619}
620
621/// The same, indented, with the children queued one level deeper.
622fn push_pretty_element<'a>(
623    element: &'a Element,
624    depth: usize,
625    out: &mut String,
626    options: &RenderOptions,
627    stack: &mut Vec<Step<'a>>,
628    in_raw_text: bool,
629) -> bool {
630    // Checked before the indent is written, so a dropped element leaves nothing behind for
631    // `DropIfEmpty` to trip over.
632    if !is_valid_tag_name(element.tag()) {
633        return false;
634    }
635    // `<pre>`, `<code>` and `<textarea>` render every whitespace character they contain, so
636    // indenting their children would change what the browser shows.
637    if is_whitespace_sensitive(element.tag()) {
638        options.write_indent(out, depth);
639        stack.push(Step::CompactElement(element));
640        return false;
641    }
642
643    options.write_indent(out, depth);
644    out.push('<');
645    out.push_str(element.tag());
646    write_attributes(element.attributes(), out, in_raw_text, element.tag());
647
648    if is_void(element.tag()) || element.is_self_closing() {
649        write_void_suffix(out, options);
650        return false;
651    }
652
653    out.push('>');
654
655    if element.children().is_empty() {
656        if let Some(content) = element.content() {
657            out.push_str(content);
658        }
659        out.push_str("</");
660        out.push_str(element.tag());
661        out.push('>');
662        return false;
663    }
664
665    if let Some(content) = element.content() {
666        out.push('\n');
667        options.write_indent(out, depth + 1);
668        out.push_str(content);
669    }
670
671    stack.push(Step::ClosePretty(element.tag(), depth));
672    stack.extend(
673        element
674            .children()
675            .iter()
676            .rev()
677            .map(|child| Step::PrettyChild {
678                node: child,
679                depth: depth + 1,
680                group_start: None,
681            }),
682    );
683    true
684}
685
686fn write_comment(out: &mut String, content: &str) {
687    // The content was neutralised when the node was built, and `Markup` has no public
688    // constructor, so a `Node::Comment` cannot hold anything else.
689    out.push_str("<!-- ");
690    out.push_str(content);
691    out.push_str(" -->");
692}
693
694/// Appends `content` with a space between adjacent `-` and after a trailing `-`.
695///
696/// A single `replace("--", "- -")` is not enough: `---` becomes `- --`, which still holds
697/// `--`, and `--->` still closes the comment.
698fn push_neutralised_comment(out: &mut String, content: &str) {
699    let mut previous_hyphen = false;
700    for c in content.chars() {
701        if c == '-' && previous_hyphen {
702            out.push(' ');
703        }
704        out.push(c);
705        previous_hyphen = c == '-';
706    }
707    if previous_hyphen {
708        out.push(' ');
709    }
710}
711
712fn write_attributes(attributes: &[Attribute], out: &mut String, in_raw_text: bool, tag: &str) {
713    for attribute in attributes {
714        attribute.write_into_in(out, in_raw_text, tag);
715    }
716}
717
718/// Appends the closing token of a void element.
719fn write_void_suffix(out: &mut String, options: &RenderOptions) {
720    out.push_str(if options.xhtml_self_closing {
721        " />"
722    } else {
723        ">"
724    });
725}
726
727#[cfg(test)]
728mod tests {
729    use std::collections::hash_map::DefaultHasher;
730    use std::hash::{Hash, Hasher};
731
732    fn hash_of(value: &impl Hash) -> u64 {
733        let mut hasher = DefaultHasher::new();
734        value.hash(&mut hasher);
735        hasher.finish()
736    }
737
738    #[test]
739    fn equal_trees_hash_equal_and_different_trees_do_not() {
740        let a = div()
741            .attr("id", "x")
742            .child(p().text("t"))
743            .child(Node::comment("c"));
744        let b = div()
745            .attr("id", "x")
746            .child(p().text("t"))
747            .child(Node::comment("c"));
748        assert_eq!(a, b);
749        assert_eq!(hash_of(&a), hash_of(&b));
750        assert_eq!(hash_of(&Node::from(a.clone())), hash_of(&Node::from(b)));
751        assert_ne!(hash_of(&a), hash_of(&a.clone().add_class("y")));
752        assert_ne!(hash_of(&Node::text("a")), hash_of(&Node::raw("a")));
753        assert_ne!(
754            hash_of(&Node::fragment([Node::text("a"), Node::text("b")])),
755            hash_of(&Node::fragment([
756                Node::fragment([Node::text("a")]),
757                Node::text("b")
758            ]))
759        );
760    }
761
762    #[test]
763    fn display_is_compact_and_alternate_is_pretty() {
764        let el = div().child(p().text("hi"));
765        assert_eq!(el.to_string(), "<div><p>hi</p></div>");
766        assert_eq!(format!("{el:#}"), "<div>\n  <p>hi</p>\n</div>");
767        let node = Node::from(el);
768        assert_eq!(node.to_string(), "<div><p>hi</p></div>");
769        assert_eq!(format!("{node:#}"), "<div>\n  <p>hi</p>\n</div>");
770    }
771
772    #[test]
773    fn nodes_collect_and_convert_from_the_common_shapes() {
774        let collected: Node = ["a", "b"].into_iter().map(Node::text).collect();
775        assert_eq!(collected.render(), "ab");
776        assert_eq!(
777            Node::from(vec![Node::text("a"), Node::text("b")]).render(),
778            "ab"
779        );
780        assert_eq!(Node::from(Some(p().text("x"))).render(), "<p>x</p>");
781        assert!(Node::from(None::<Element>).is_empty());
782        assert_eq!(Node::from('<').render(), "&lt;");
783
784        let mut list = ul();
785        list.extend([li().text("1"), li().text("2")]);
786        list.extend(["tail"]);
787        assert_eq!(list.render(), "<ul><li>1</li><li>2</li>tail</ul>");
788    }
789
790    #[test]
791    fn references_boxes_and_options_render_like_what_they_hold() {
792        // Through a generic bound, so the impl for the wrapper (`&T`, `Box<T>`, `Option<T>`) is
793        // what runs. Calling `.render()` on a `Box<Element>` would auto-deref to the element's
794        // own method and test nothing about the `Box` impl.
795        fn through<T: Render>(value: T) -> String {
796            value.render()
797        }
798        let el = p().text("x");
799        assert_eq!(through(&el), "<p>x</p>");
800        assert_eq!(through(Box::new(el.clone())), "<p>x</p>");
801        assert_eq!(through(Some(el.clone())), "<p>x</p>");
802        assert_eq!(through(None::<Element>), "");
803        let dynamic: Box<dyn Render> = Box::new(el);
804        assert_eq!(dynamic.render(), "<p>x</p>");
805    }
806
807    #[test]
808    fn a_document_has_a_default_and_a_display() {
809        let page = crate::document::Document::default();
810        assert_eq!(page.to_string(), page.render());
811        assert!(page.to_string().starts_with("<!DOCTYPE html>\n<html>"));
812    }
813
814    use super::*;
815    use crate::elements::{
816        body, code, div, h1, head, html_tag, i, img, li, p, pre, span, textarea, title, ul,
817    };
818    // `macros` is declared after `core` in lib.rs, so `html!` is not in scope by position;
819    // it is `#[macro_export]`ed, which puts it at the crate root.
820    use crate::html;
821
822    #[test]
823    fn the_tree_is_send_and_sync() {
824        fn assert_send_sync<T: Send + Sync>() {}
825        assert_send_sync::<Node>();
826        assert_send_sync::<Element>();
827    }
828
829    #[test]
830    fn text_nodes_are_escaped() {
831        assert_eq!(Node::text("a & b").render(), "a &amp; b");
832    }
833
834    /// The node-level escape hatch; `Element::raw_text` is the element-level one.
835    #[test]
836    fn raw_nodes_are_never_escaped() {
837        assert_eq!(Node::raw("<b>bold</b>").render(), "<b>bold</b>");
838        assert_eq!(Node::raw("<b>bold</b>").render_pretty(), "<b>bold</b>");
839    }
840
841    #[test]
842    fn a_comment_cannot_close_itself_early() {
843        let rendered = Node::comment("a --> b").render();
844        assert_eq!(rendered.matches("-->").count(), 1);
845        assert!(rendered.ends_with("-->"));
846    }
847
848    /// `replace("--", "- -")` turned `--->` into `- -->`, which still closed the comment.
849    /// `Element::new` takes any string, and the tag is written verbatim between `<` and `>`.
850    #[test]
851    fn an_unsafe_tag_name_renders_nothing() {
852        for hostile in [
853            "img src=x onerror=alert(1)",
854            "div><script>alert(1)</script><div",
855            "a\"b",
856            "",
857            "1div",
858            "p\n",
859        ] {
860            let tree = div()
861                .child(Element::new(hostile).text("kept?"))
862                .child(p().text("after"));
863            assert_eq!(tree.render(), "<div><p>after</p></div>", "{hostile:?}");
864            assert_eq!(
865                tree.render_pretty(),
866                "<div>\n  <p>after</p>\n</div>",
867                "{hostile:?} must leave no blank line in pretty mode"
868            );
869            assert_eq!(Element::new(hostile).render(), "", "{hostile:?}");
870        }
871    }
872
873    #[test]
874    fn custom_and_namespaced_tags_still_render() {
875        assert_eq!(
876            Element::new("my-card").text("x").render(),
877            "<my-card>x</my-card>"
878        );
879        assert_eq!(Element::new("svg:rect").render(), "<svg:rect></svg:rect>");
880    }
881
882    /// Inside `<noscript>` the parser reads raw text, so `</noscript>` in a descendant's
883    /// attribute value ends the element. Elsewhere `<` in a quoted value is inert, and the
884    /// golden fixtures depend on it staying unescaped.
885    #[test]
886    fn angle_brackets_in_attributes_are_escaped_only_under_raw_text_parents() {
887        let hostile = "</noscript><img src=x onerror=alert(1)>";
888        let escaped = "&lt;/noscript&gt;&lt;img src=x onerror=alert(1)&gt;";
889
890        for parent in [
891            "noscript", "iframe", "xmp", "noembed", "noframes", "style", "script",
892        ] {
893            let tree = Element::new(parent).child(img().attr("alt", hostile));
894            assert_eq!(
895                tree.render(),
896                format!(r#"<{parent}><img alt="{escaped}"></{parent}>"#),
897                "{parent}"
898            );
899        }
900
901        assert_eq!(
902            div().child(img().attr("alt", hostile)).render(),
903            format!(r#"<div><img alt="{hostile}"></div>"#),
904            "ordinary parents leave `<` and `>` in an attribute value alone"
905        );
906    }
907
908    #[test]
909    fn the_raw_text_escape_covers_deep_descendants_and_ends_with_the_parent() {
910        let hostile = "a<b>c";
911        let tree = div()
912            .child(
913                Element::new("noscript")
914                    .attr("title", hostile)
915                    .child(div().child(span().child(img().attr("alt", hostile)))),
916            )
917            .child(img().attr("alt", hostile));
918        // The noscript's own attribute is read before raw-text mode starts, so it keeps the
919        // plain form; its descendant is escaped; the sibling after it is plain again.
920        assert_eq!(
921            tree.render(),
922            concat!(
923                r#"<div><noscript title="a<b>c"><div><span>"#,
924                r#"<img alt="a&lt;b&gt;c"></span></div></noscript>"#,
925                r#"<img alt="a<b>c"></div>"#
926            )
927        );
928        assert_eq!(tree.render_pretty().matches("a&lt;b&gt;c").count(), 1);
929        assert_eq!(tree.render_pretty().matches("a<b>c").count(), 2);
930    }
931
932    #[test]
933    fn a_comment_survives_runs_of_hyphens_and_bang_closers() {
934        for hostile in [
935            "x---><img src=x onerror=alert(1)>",
936            "--!><b>",
937            "a-",
938            "---",
939            "-->",
940            "--",
941        ] {
942            let rendered = Node::comment(hostile).render();
943            let inner = rendered
944                .strip_prefix("<!-- ")
945                .and_then(|r| r.strip_suffix(" -->"));
946            let inner = inner.expect("comment keeps its delimiters");
947            assert!(!inner.contains("--"), "{hostile:?} -> {rendered:?}");
948            assert!(!inner.ends_with('-'), "{hostile:?} -> {rendered:?}");
949        }
950    }
951
952    #[test]
953    fn an_empty_fragment_leaves_no_blank_line() {
954        let tree = div()
955            .child(p().text("a"))
956            .child(Node::fragment([]))
957            .child(p().text("b"));
958        assert_eq!(
959            tree.render_pretty(),
960            "<div>\n  <p>a</p>\n  <p>b</p>\n</div>"
961        );
962    }
963
964    #[test]
965    fn a_fragment_renders_its_children_without_a_wrapper() {
966        let tree = Node::fragment([p().text("a").into(), p().text("b").into()]);
967        assert_eq!(tree.render(), "<p>a</p><p>b</p>");
968        assert_eq!(tree.render_pretty(), "<p>a</p>\n<p>b</p>");
969    }
970
971    #[test]
972    fn void_elements_take_no_children() {
973        let tree = img().attr("src", "/a.png").child(span().text("dropped"));
974        assert_eq!(tree.render(), r#"<img src="/a.png">"#);
975    }
976
977    #[test]
978    fn xhtml_mode_closes_void_elements_with_a_slash() {
979        let options = RenderOptions::compact().with_xhtml_self_closing(true);
980        assert_eq!(
981            img().attr("src", "/a.png").render_with(&options),
982            r#"<img src="/a.png" />"#
983        );
984    }
985
986    /// Indenting inside `<pre>` would change what the browser shows.
987    #[test]
988    fn whitespace_sensitive_tags_are_not_indented_inside() {
989        let tree = div().child(pre().child(code().text("let page = html { }")));
990        assert_eq!(
991            tree.render_pretty(),
992            "<div>\n  <pre><code>let page = html { }</code></pre>\n</div>"
993        );
994    }
995
996    /// Content and children can be mixed; the content comes first, on its own line.
997    #[test]
998    fn content_goes_on_its_own_line_before_children() {
999        let tree = div().text("lead").child(p().text("body"));
1000        assert_eq!(tree.render_pretty(), "<div>\n  lead\n  <p>body</p>\n</div>");
1001    }
1002
1003    #[test]
1004    fn an_element_with_content_and_no_children_stays_on_one_line() {
1005        assert_eq!(p().text("hi").render_pretty(), "<p>hi</p>");
1006    }
1007
1008    #[test]
1009    fn compact_is_the_default_for_an_element() {
1010        let tree = div().child(p().text("a"));
1011        assert_eq!(tree.render(), "<div><p>a</p></div>");
1012    }
1013
1014    #[test]
1015    fn raw_markup_renders_with_no_wrapper_around_it() {
1016        let raw = Node::raw(r#"<span class="x">hi</span>"#);
1017
1018        assert_eq!(raw.render(), r#"<span class="x">hi</span>"#);
1019        assert!(!raw.render().contains("<div"));
1020    }
1021
1022    #[test]
1023    fn raw_markup_as_a_child_adds_no_wrapper() {
1024        let markup = div()
1025            .child(Node::raw(r#"<i class="fa fa-home"></i>"#))
1026            .child(span().text("Home"));
1027
1028        assert_eq!(
1029            markup.render(),
1030            r#"<div><i class="fa fa-home"></i><span>Home</span></div>"#
1031        );
1032    }
1033
1034    #[test]
1035    fn a_fragment_renders_its_children_with_no_wrapper() {
1036        let fragment = Node::fragment([
1037            i().add_class("fa fa-star").into(),
1038            span().text(" Featured").into(),
1039        ]);
1040
1041        assert_eq!(
1042            fragment.render(),
1043            r#"<i class="fa fa-star"></i><span> Featured</span>"#
1044        );
1045    }
1046
1047    #[test]
1048    fn a_fragment_takes_a_mapped_sequence() {
1049        let fragment = Node::fragment(
1050            ["One", "Two", "Three"].map(|title| div().add_class("card").text(title).into()),
1051        );
1052
1053        assert_eq!(
1054            fragment.render(),
1055            concat!(
1056                r#"<div class="card">One</div><div class="card">Two</div>"#,
1057                r#"<div class="card">Three</div>"#,
1058            )
1059        );
1060    }
1061
1062    // These pin the empty-fragment behaviour the pretty writer exists to preserve, so they are
1063    // also the independent check on the iterative rewrite.
1064
1065    #[test]
1066    fn an_empty_fragment_renders_nothing_in_either_mode() {
1067        assert_eq!(Node::fragment([]).render(), "");
1068        assert_eq!(Node::fragment([]).render_pretty(), "");
1069    }
1070
1071    #[test]
1072    fn a_fragments_children_are_indented_as_the_parents_own() {
1073        let list = ul().child(Node::fragment([
1074            li().text("a").into(),
1075            li().text("b").into(),
1076        ]));
1077
1078        assert_eq!(
1079            list.render_pretty(),
1080            "<ul>\n  <li>a</li>\n  <li>b</li>\n</ul>"
1081        );
1082    }
1083
1084    #[test]
1085    fn a_fragment_takes_a_filtered_and_mapped_sequence() {
1086        let names = ["Ana", "Bruno", "Carla"];
1087        let group = Node::fragment(
1088            names
1089                .iter()
1090                .filter(|name| name.len() > 3)
1091                .map(|name| li().text(name).into()),
1092        );
1093
1094        assert_eq!(group.render(), "<li>Bruno</li><li>Carla</li>");
1095    }
1096
1097    #[test]
1098    fn a_false_condition_emits_no_stray_node() {
1099        let show_banner = false;
1100        let page = html_tag()
1101            .child(head().child(title().text("Home")))
1102            .child(html! { @if show_banner { div { "banner" } } })
1103            .child(body().child(h1().text("Hi")));
1104
1105        assert_eq!(
1106            page.render(),
1107            "<html><head><title>Home</title></head><body><h1>Hi</h1></body></html>"
1108        );
1109    }
1110
1111    #[test]
1112    fn a_true_condition_emits_its_branch() {
1113        let show_banner = true;
1114        let page = html_tag().child(html! { @if show_banner { div { "banner" } } });
1115
1116        assert_eq!(page.render(), "<html><div>banner</div></html>");
1117    }
1118
1119    #[test]
1120    fn a_loop_emits_one_node_per_iteration() {
1121        let page = html_tag().child(html! {
1122            @for index in 1..=3 { p { "line " (index) } }
1123        });
1124
1125        assert_eq!(
1126            page.render(),
1127            "<html><p>line 1</p><p>line 2</p><p>line 3</p></html>"
1128        );
1129    }
1130
1131    #[test]
1132    fn raw_markup_is_indented_as_one_blob() {
1133        let container = div().child(Node::raw("<custom-element></custom-element>"));
1134
1135        assert_eq!(
1136            container.render_pretty(),
1137            "<div>\n  <custom-element></custom-element>\n</div>"
1138        );
1139    }
1140
1141    #[test]
1142    fn a_condition_can_pick_either_branch() {
1143        fn badge(is_beta: bool) -> Node {
1144            html! { @if is_beta { span { "beta" } } @else { span { "stable" } } }
1145        }
1146
1147        assert_eq!(badge(true).render(), "<span>beta</span>");
1148        assert_eq!(badge(false).render(), "<span>stable</span>");
1149    }
1150
1151    /// Two levels of nesting with an empty fragment between them: the rollback has to
1152    /// survive being nested inside another rollback.
1153    #[test]
1154    fn nested_fragments_flatten_without_leaving_gaps() {
1155        let list = ul().child(Node::fragment([
1156            Node::fragment([li().text("a").into()]),
1157            Node::fragment([]),
1158            li().text("b").into(),
1159        ]));
1160
1161        assert_eq!(
1162            list.render_pretty(),
1163            "<ul>\n  <li>a</li>\n  <li>b</li>\n</ul>"
1164        );
1165    }
1166
1167    #[test]
1168    fn raw_markup_is_emitted_verbatim() {
1169        let raw = Node::raw(r#"<custom-element data-x="1"></custom-element>"#);
1170
1171        assert_eq!(
1172            raw.render(),
1173            r#"<custom-element data-x="1"></custom-element>"#
1174        );
1175    }
1176
1177    #[test]
1178    fn a_tag_with_only_content_stays_on_one_line_when_pretty() {
1179        assert_eq!(
1180            div().text("Hello World").render_pretty(),
1181            "<div>Hello World</div>"
1182        );
1183    }
1184
1185    #[test]
1186    fn children_each_get_their_own_line_when_pretty() {
1187        let tree = div()
1188            .child(p().text("Paragraph 1"))
1189            .child(p().text("Paragraph 2"));
1190
1191        assert_eq!(
1192            tree.render_pretty(),
1193            "<div>\n  <p>Paragraph 1</p>\n  <p>Paragraph 2</p>\n</div>"
1194        );
1195    }
1196
1197    #[test]
1198    fn compact_keeps_everything_on_one_line() {
1199        assert_eq!(
1200            div().child(p().text("Test")).render(),
1201            "<div><p>Test</p></div>"
1202        );
1203    }
1204
1205    #[test]
1206    fn a_void_element_does_not_self_close_when_pretty() {
1207        let rendered = img()
1208            .attr("src", "test.jpg")
1209            .attr("alt", "Test")
1210            .render_pretty();
1211
1212        assert!(rendered.starts_with("<img"));
1213        assert!(!rendered.contains("/>"));
1214        assert!(rendered.ends_with('>'));
1215    }
1216
1217    #[test]
1218    fn a_textarea_keeps_its_newlines() {
1219        let field = textarea().attr("name", "bio").text("line 1\nline 2");
1220
1221        assert_eq!(
1222            field.render_pretty(),
1223            "<textarea name=\"bio\">line 1\nline 2</textarea>"
1224        );
1225    }
1226
1227    #[test]
1228    fn a_pre_block_is_indented_from_the_outside_but_not_within() {
1229        let container = div().child(pre().child(code().text("cargo build")));
1230
1231        assert_eq!(
1232            container.render_pretty(),
1233            "<div>\n  <pre><code>cargo build</code></pre>\n</div>"
1234        );
1235    }
1236}