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