Skip to main content

winged_rust/core/
node.rs

1//! The node tree.
2//!
3//! Ports `core/HTMLTag.swift` (state), `core/Fragment.swift` and `core/RawHTML.swift`.
4//!
5//! Winged-Swift models fragments and raw markup as **subclasses** of `HTMLTag` that
6//! override `write(into:)`. Rust models the same thing as a closed enum with a `match` in
7//! the writer. That is strictly better here: the tree becomes `Send + Sync` for free,
8//! which Winged-Swift's own `ROADMAP.md` lists as an unresolved 3.0 problem ("tag trees
9//! are not `Sendable`"), and which is what makes the `parallel` feature sound.
10
11use crate::core::attribute::Attribute;
12use crate::core::element::Element;
13use crate::core::escape::write_escaped_text;
14use crate::core::render::{Render, RenderOptions};
15use crate::core::tags::{is_void, is_whitespace_sensitive};
16
17/// A node in the HTML tree.
18///
19/// # Examples
20/// ```
21/// use winged_rust::prelude::*;
22/// let node = Node::from(div().text("hi"));
23/// assert_eq!(node.render(), "<div>hi</div>");
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Node {
27    /// An element with a tag name, attributes and children.
28    Element(Element),
29    /// Escaped text. The escaping already happened — see [`crate::core::escape`].
30    Text(String),
31    /// Verbatim markup, never escaped. For imported snippets: an SVG, an embed code.
32    ///
33    /// Unlike [`Node::Fragment`], this is an opaque string, so pretty printing can only
34    /// indent the whole blob. Reach for a fragment when you want the children indented.
35    Raw(String),
36    /// An HTML comment. **New in the Rust port** — Winged-Swift has no comment node.
37    ///
38    /// The content has `--` neutralised so the comment cannot be closed early.
39    Comment(String),
40    /// A transparent group that renders its children with no wrapper element.
41    ///
42    /// Use it to return several nodes from one expression — a `map` of cards, the body of
43    /// an `if`, a group of `<meta>` tags — without introducing an extra `<div>`.
44    Fragment(Vec<Node>),
45}
46
47impl Node {
48    /// Creates a text node, escaping the content.
49    #[must_use]
50    pub fn text(content: impl AsRef<str>) -> Self {
51        let raw = content.as_ref();
52        let mut escaped = String::with_capacity(raw.len());
53        write_escaped_text(&mut escaped, raw, false);
54        Self::Text(escaped)
55    }
56
57    /// Creates a raw markup node. The content is **not** escaped.
58    #[must_use]
59    pub fn raw(content: impl Into<String>) -> Self {
60        Self::Raw(content.into())
61    }
62
63    /// Creates a comment node.
64    ///
65    /// Any `--` in the content is replaced with `- -`, so the comment cannot terminate
66    /// early and inject markup.
67    ///
68    /// # Examples
69    /// ```
70    /// use winged_rust::prelude::*;
71    /// assert_eq!(Node::comment("a -- b").render(), "<!-- a - - b -->");
72    /// ```
73    #[must_use]
74    pub fn comment(content: impl AsRef<str>) -> Self {
75        Self::Comment(content.as_ref().replace("--", "- -"))
76    }
77
78    /// Creates a transparent group of nodes.
79    #[must_use]
80    pub fn fragment(children: impl IntoIterator<Item = Node>) -> Self {
81        Self::Fragment(children.into_iter().collect())
82    }
83
84    /// Whether this node renders to nothing.
85    ///
86    /// The pretty-print writer arrives at the same answer without calling this: it lets a
87    /// child write straight into the output and rolls the separator back if the child wrote
88    /// nothing, which is what stops an empty fragment — the body of a false `if` — from
89    /// leaving a blank line behind.
90    #[must_use]
91    pub fn is_empty(&self) -> bool {
92        match self {
93            Self::Element(_) | Self::Comment(_) => false,
94            Self::Text(s) | Self::Raw(s) => s.is_empty(),
95            Self::Fragment(children) => children.iter().all(Self::is_empty),
96        }
97    }
98}
99
100impl From<Element> for Node {
101    fn from(element: Element) -> Self {
102        Self::Element(element)
103    }
104}
105
106impl From<&str> for Node {
107    fn from(text: &str) -> Self {
108        Self::text(text)
109    }
110}
111
112impl From<String> for Node {
113    fn from(text: String) -> Self {
114        Self::text(text)
115    }
116}
117
118impl Render for Node {
119    fn write_into(&self, out: &mut String, options: &RenderOptions, depth: usize) {
120        write_tree(
121            if options.pretty {
122                Step::Pretty(self, depth)
123            } else {
124                Step::Compact(self)
125            },
126            out,
127            options,
128        );
129    }
130}
131
132/// Renders an element without wrapping it in a [`Node`] first.
133///
134/// [`Render for Element`](Element) used to do `Node::Element(self.clone()).write_into(..)`,
135/// which deep-copied the whole subtree on every render — and cloning is itself recursive,
136/// so a deep tree aborted in `Clone` before the writer ever saw it.
137pub(crate) fn write_element_tree(
138    element: &Element,
139    out: &mut String,
140    options: &RenderOptions,
141    depth: usize,
142) {
143    write_tree(
144        if options.pretty {
145            Step::PrettyElement(element, depth)
146        } else {
147            Step::CompactElement(element)
148        },
149        out,
150        options,
151    );
152}
153
154/// One unit of work for the writer.
155///
156/// The writer used to recurse once per nesting level, which made tree depth a stack
157/// limit: roughly 2,000 levels aborted the process, and a stack overflow is an abort, not
158/// a panic anything can catch. Each variant here is what one of those recursive calls
159/// used to do, held on an explicit stack instead — so depth costs heap, bounded by a tree
160/// that is already in memory.
161enum Step<'a> {
162    /// Write this node on one line.
163    Compact(&'a Node),
164    /// Write this node indented to `depth`.
165    Pretty(&'a Node, usize),
166    /// Write this element on one line.
167    CompactElement(&'a Element),
168    /// Write this element indented to `depth`.
169    PrettyElement(&'a Element, usize),
170    /// `</tag>`.
171    Close(&'a str),
172    /// A newline, indentation to `depth`, then `</tag>`.
173    ClosePretty(&'a str, usize),
174    /// A child in pretty mode: writes the separating newline, renders the child, and then
175    /// takes both back out if the child rendered to nothing.
176    ///
177    /// `group_start` is `Some` inside a fragment, where the separator is only written once
178    /// something has already been emitted, and `None` inside an element, where every child
179    /// gets one.
180    PrettyChild {
181        node: &'a Node,
182        depth: usize,
183        group_start: Option<usize>,
184    },
185    /// Truncates back to `mark` when nothing was appended after `after`.
186    ///
187    /// This replaces rendering each child into a scratch buffer to test it for emptiness:
188    /// the child writes straight into the output and its separator is rolled back if it
189    /// wrote nothing, which is what still keeps an empty fragment — the body of a false
190    /// `@if` — from leaving a blank line behind.
191    DropIfEmpty { mark: usize, after: usize },
192}
193
194/// Drives the steps until the tree is written.
195fn write_tree(start: Step<'_>, out: &mut String, options: &RenderOptions) {
196    let mut stack = Vec::with_capacity(16);
197    stack.push(start);
198
199    while let Some(step) = stack.pop() {
200        match step {
201            // Escaped text and verbatim markup write the same way; they differ only in
202            // when the escaping happened, which is at construction.
203            Step::Compact(Node::Text(text) | Node::Raw(text)) => out.push_str(text),
204            Step::Compact(Node::Comment(content)) => write_comment(out, content),
205            Step::Compact(Node::Fragment(children)) => {
206                stack.extend(children.iter().rev().map(Step::Compact));
207            }
208            Step::Compact(Node::Element(element)) => stack.push(Step::CompactElement(element)),
209
210            Step::Pretty(Node::Text(text) | Node::Raw(text), depth) => {
211                options.write_indent(out, depth);
212                out.push_str(text);
213            }
214            Step::Pretty(Node::Comment(content), depth) => {
215                options.write_indent(out, depth);
216                write_comment(out, content);
217            }
218            Step::Pretty(Node::Fragment(children), depth) => {
219                // The children sit at the *parent's* depth, joined by newlines, with empty
220                // ones skipped entirely.
221                let group_start = out.len();
222                stack.extend(children.iter().rev().map(|child| Step::PrettyChild {
223                    node: child,
224                    depth,
225                    group_start: Some(group_start),
226                }));
227            }
228            Step::Pretty(Node::Element(element), depth) => {
229                stack.push(Step::PrettyElement(element, depth));
230            }
231
232            Step::CompactElement(element) => {
233                push_compact_element(element, out, options, &mut stack);
234            }
235            Step::PrettyElement(element, depth) => {
236                push_pretty_element(element, depth, out, options, &mut stack);
237            }
238
239            Step::Close(tag) => {
240                out.push_str("</");
241                out.push_str(tag);
242                out.push('>');
243            }
244            Step::ClosePretty(tag, depth) => {
245                out.push('\n');
246                options.write_indent(out, depth);
247                out.push_str("</");
248                out.push_str(tag);
249                out.push('>');
250            }
251
252            Step::PrettyChild {
253                node,
254                depth,
255                group_start,
256            } => {
257                let mark = out.len();
258                let needs_separator = group_start.is_none_or(|start| out.len() > start);
259                if needs_separator {
260                    out.push('\n');
261                }
262                let after = out.len();
263                // Pushed first so it runs last: the child goes on top of it.
264                stack.push(Step::DropIfEmpty { mark, after });
265                stack.push(Step::Pretty(node, depth));
266            }
267            Step::DropIfEmpty { mark, after } => {
268                if out.len() == after {
269                    out.truncate(mark);
270                }
271            }
272        }
273    }
274}
275
276/// Writes an element's opening token and queues everything that follows it.
277fn push_compact_element<'a>(
278    element: &'a Element,
279    out: &mut String,
280    options: &RenderOptions,
281    stack: &mut Vec<Step<'a>>,
282) {
283    out.push('<');
284    out.push_str(element.tag());
285    write_attributes(element.attributes(), out);
286
287    // A void element takes no content and no children — both are silently dropped, which
288    // is what Winged-Swift does.
289    if is_void(element.tag()) {
290        write_void_suffix(out, options);
291        return;
292    }
293
294    out.push('>');
295    if let Some(content) = element.content() {
296        out.push_str(content);
297    }
298    stack.push(Step::Close(element.tag()));
299    stack.extend(element.children().iter().rev().map(Step::Compact));
300}
301
302/// The same, indented, with the children queued one level deeper.
303fn push_pretty_element<'a>(
304    element: &'a Element,
305    depth: usize,
306    out: &mut String,
307    options: &RenderOptions,
308    stack: &mut Vec<Step<'a>>,
309) {
310    // `<pre>`, `<code>` and `<textarea>` render every whitespace character they contain, so
311    // indenting their children would change what the browser shows.
312    if is_whitespace_sensitive(element.tag()) {
313        options.write_indent(out, depth);
314        stack.push(Step::CompactElement(element));
315        return;
316    }
317
318    options.write_indent(out, depth);
319    out.push('<');
320    out.push_str(element.tag());
321    write_attributes(element.attributes(), out);
322
323    if is_void(element.tag()) {
324        write_void_suffix(out, options);
325        return;
326    }
327
328    out.push('>');
329
330    if element.children().is_empty() {
331        if let Some(content) = element.content() {
332            out.push_str(content);
333        }
334        out.push_str("</");
335        out.push_str(element.tag());
336        out.push('>');
337        return;
338    }
339
340    if let Some(content) = element.content() {
341        out.push('\n');
342        options.write_indent(out, depth + 1);
343        out.push_str(content);
344    }
345
346    stack.push(Step::ClosePretty(element.tag(), depth));
347    stack.extend(
348        element
349            .children()
350            .iter()
351            .rev()
352            .map(|child| Step::PrettyChild {
353                node: child,
354                depth: depth + 1,
355                group_start: None,
356            }),
357    );
358}
359
360fn write_comment(out: &mut String, content: &str) {
361    out.push_str("<!-- ");
362    out.push_str(content);
363    out.push_str(" -->");
364}
365
366fn write_attributes(attributes: &[Attribute], out: &mut String) {
367    for attribute in attributes {
368        attribute.write_into(out);
369    }
370}
371
372/// Appends the closing token of a void element.
373fn write_void_suffix(out: &mut String, options: &RenderOptions) {
374    out.push_str(if options.xhtml_self_closing {
375        " />"
376    } else {
377        ">"
378    });
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::elements::{
385        body, code, div, h1, head, html_tag, i, img, li, p, pre, span, textarea, title, ul,
386    };
387    // `macros` is declared after `core` in lib.rs, so `html!` is not in scope by position;
388    // it is `#[macro_export]`ed, which puts it at the crate root.
389    use crate::html;
390
391    #[test]
392    fn the_tree_is_send_and_sync() {
393        fn assert_send_sync<T: Send + Sync>() {}
394        assert_send_sync::<Node>();
395        assert_send_sync::<Element>();
396    }
397
398    #[test]
399    fn text_nodes_are_escaped() {
400        assert_eq!(Node::text("a & b").render(), "a &amp; b");
401    }
402
403    /// No direct Swift counterpart: Winged-Swift turns escaping off per tag with
404    /// `escapeContent:`, which `Element::raw_text` ports. This is the node-level version,
405    /// which has no equivalent because Swift models raw markup as an `HTMLTag` subclass.
406    #[test]
407    fn raw_nodes_are_never_escaped() {
408        assert_eq!(Node::raw("<b>bold</b>").render(), "<b>bold</b>");
409        assert_eq!(Node::raw("<b>bold</b>").render_pretty(), "<b>bold</b>");
410    }
411
412    #[test]
413    fn a_comment_cannot_close_itself_early() {
414        let rendered = Node::comment("a --> b").render();
415        assert_eq!(rendered.matches("-->").count(), 1);
416        assert!(rendered.ends_with("-->"));
417    }
418
419    /// Ports `FragmentTests.testEmptyFragmentDoesNotLeaveBlankLines`.
420    #[test]
421    fn an_empty_fragment_leaves_no_blank_line() {
422        let tree = div()
423            .child(p().text("a"))
424            .child(Node::fragment([]))
425            .child(p().text("b"));
426        assert_eq!(
427            tree.render_pretty(),
428            "<div>\n  <p>a</p>\n  <p>b</p>\n</div>"
429        );
430    }
431
432    /// Ports `FragmentTests.testFragmentRendersChildrenWithoutWrapper`.
433    #[test]
434    fn a_fragment_renders_its_children_without_a_wrapper() {
435        let tree = Node::fragment([p().text("a").into(), p().text("b").into()]);
436        assert_eq!(tree.render(), "<p>a</p><p>b</p>");
437        assert_eq!(tree.render_pretty(), "<p>a</p>\n<p>b</p>");
438    }
439
440    /// Ports the void-element cases in `TagCatalogTests`.
441    #[test]
442    fn void_elements_take_no_children() {
443        let tree = img().attr("src", "/a.png").child(span().text("dropped"));
444        assert_eq!(tree.render(), r#"<img src="/a.png">"#);
445    }
446
447    #[test]
448    fn xhtml_mode_closes_void_elements_with_a_slash() {
449        let options = RenderOptions::compact().with_xhtml_self_closing(true);
450        assert_eq!(
451            img().attr("src", "/a.png").render_with(&options),
452            r#"<img src="/a.png" />"#
453        );
454    }
455
456    /// Ports `WhitespaceTests.testPreWithCodeChildIsNotIndented` and
457    /// `WhitespaceTests.testPrettyMatchesCompactForWhitespaceSensitiveTags`. Indenting inside `<pre>` would change what the browser shows.
458    #[test]
459    fn whitespace_sensitive_tags_are_not_indented_inside() {
460        let tree = div().child(pre().child(code().text("let page = html { }")));
461        assert_eq!(
462            tree.render_pretty(),
463            "<div>\n  <pre><code>let page = html { }</code></pre>\n</div>"
464        );
465    }
466
467    /// No Swift counterpart: its `PrettyPrintTests` never mixes content with children.
468    #[test]
469    fn content_goes_on_its_own_line_before_children() {
470        let tree = div().text("lead").child(p().text("body"));
471        assert_eq!(tree.render_pretty(), "<div>\n  lead\n  <p>body</p>\n</div>");
472    }
473
474    #[test]
475    fn an_element_with_content_and_no_children_stays_on_one_line() {
476        assert_eq!(p().text("hi").render_pretty(), "<p>hi</p>");
477    }
478
479    #[test]
480    fn compact_is_the_default_for_an_element() {
481        let tree = div().child(p().text("a"));
482        assert_eq!(tree.render(), "<div><p>a</p></div>");
483    }
484
485    // Ports the `RawHTML` and fragment half of `HTML14FeaturesTests`; the element half
486    // lives in `crate::elements`.
487
488    /// Ports `HTML14FeaturesTests.testRawHTMLRendersWithoutWrapper`.
489    #[test]
490    fn raw_markup_renders_with_no_wrapper_around_it() {
491        let raw = Node::raw(r#"<span class="x">hi</span>"#);
492
493        assert_eq!(raw.render(), r#"<span class="x">hi</span>"#);
494        assert!(!raw.render().contains("<div"));
495    }
496
497    /// Ports `HTML14FeaturesTests.testRawHTMLAsChildHasNoWrapper`.
498    #[test]
499    fn raw_markup_as_a_child_adds_no_wrapper() {
500        let markup = div()
501            .child(Node::raw(r#"<i class="fa fa-home"></i>"#))
502            .child(span().text("Home"));
503
504        assert_eq!(
505            markup.render(),
506            r#"<div><i class="fa fa-home"></i><span>Home</span></div>"#
507        );
508    }
509
510    /// Ports `HTML14FeaturesTests.testFragmentHelper`.
511    #[test]
512    fn a_fragment_renders_its_children_with_no_wrapper() {
513        let fragment = Node::fragment([
514            i().add_class("fa fa-star").into(),
515            span().text(" Featured").into(),
516        ]);
517
518        assert_eq!(
519            fragment.render(),
520            r#"<i class="fa fa-star"></i><span> Featured</span>"#
521        );
522    }
523
524    /// Ports `HTML14FeaturesTests.testFragmentBuilderBuildArray`.
525    #[test]
526    fn a_fragment_takes_a_mapped_sequence() {
527        let fragment = Node::fragment(
528            ["One", "Two", "Three"].map(|title| div().add_class("card").text(title).into()),
529        );
530
531        assert_eq!(
532            fragment.render(),
533            concat!(
534                r#"<div class="card">One</div><div class="card">Two</div>"#,
535                r#"<div class="card">Three</div>"#,
536            )
537        );
538    }
539
540    // Ports `FragmentTests`. This is the suite that pins the empty-fragment behaviour the
541    // pretty writer exists to preserve, so it is also the independent check on the
542    // iterative rewrite.
543
544    /// Ports `FragmentTests.testEmptyFragmentRendersNothing`.
545    #[test]
546    fn an_empty_fragment_renders_nothing_in_either_mode() {
547        assert!(Node::fragment([]).render().is_empty());
548        assert!(Node::fragment([]).render_pretty().is_empty());
549    }
550
551    /// Ports `FragmentTests.testFragmentKeepsPrettyIndentation`.
552    #[test]
553    fn a_fragments_children_are_indented_as_the_parents_own() {
554        let list = ul().child(Node::fragment([
555            li().text("a").into(),
556            li().text("b").into(),
557        ]));
558
559        assert_eq!(
560            list.render_pretty(),
561            "<ul>\n  <li>a</li>\n  <li>b</li>\n</ul>"
562        );
563    }
564
565    /// Ports `FragmentTests.testFragmentBuilderSupportsMapAndFilter`.
566    #[test]
567    fn a_fragment_takes_a_filtered_and_mapped_sequence() {
568        let names = ["Ana", "Bruno", "Carla"];
569        let group = Node::fragment(
570            names
571                .iter()
572                .filter(|name| name.len() > 3)
573                .map(|name| li().text(name).into()),
574        );
575
576        assert_eq!(group.render(), "<li>Bruno</li><li>Carla</li>");
577    }
578
579    /// Ports `FragmentTests.testFalseConditionDoesNotEmitStrayHTMLNode`.
580    #[test]
581    fn a_false_condition_emits_no_stray_node() {
582        let show_banner = false;
583        let page = html_tag()
584            .child(head().child(title().text("Home")))
585            .child(html! { @if show_banner { div { "banner" } } })
586            .child(body().child(h1().text("Hi")));
587
588        assert_eq!(
589            page.render(),
590            "<html><head><title>Home</title></head><body><h1>Hi</h1></body></html>"
591        );
592    }
593
594    /// Ports `FragmentTests.testTrueConditionEmitsTheBranch`.
595    #[test]
596    fn a_true_condition_emits_its_branch() {
597        let show_banner = true;
598        let page = html_tag().child(html! { @if show_banner { div { "banner" } } });
599
600        assert_eq!(page.render(), "<html><div>banner</div></html>");
601    }
602
603    /// Ports `FragmentTests.testLoopInsideHTMLBuilder`.
604    #[test]
605    fn a_loop_emits_one_node_per_iteration() {
606        let page = html_tag().child(html! {
607            @for index in 1..=3 { p { "line " (index) } }
608        });
609
610        assert_eq!(
611            page.render(),
612            "<html><p>line 1</p><p>line 2</p><p>line 3</p></html>"
613        );
614    }
615
616    /// Ports `FragmentTests.testRawHTMLIsIndentedInsideAPrettyTree`.
617    #[test]
618    fn raw_markup_is_indented_as_one_blob() {
619        let container = div().child(Node::raw("<custom-element></custom-element>"));
620
621        assert_eq!(
622            container.render_pretty(),
623            "<div>\n  <custom-element></custom-element>\n</div>"
624        );
625    }
626
627    /// Ports `FragmentTests.testFragmentBuilderTakesBothBranchesOfAnIf`.
628    #[test]
629    fn a_condition_can_pick_either_branch() {
630        fn badge(is_beta: bool) -> Node {
631            html! { @if is_beta { span { "beta" } } @else { span { "stable" } } }
632        }
633
634        assert_eq!(badge(true).render(), "<span>beta</span>");
635        assert_eq!(badge(false).render(), "<span>stable</span>");
636    }
637
638    /// Ports `FragmentTests.testNestedFragmentsFlattenInPrettyOutput`.
639    ///
640    /// Two levels of nesting with an empty fragment between them: the rollback has to
641    /// survive being nested inside another rollback.
642    #[test]
643    fn nested_fragments_flatten_without_leaving_gaps() {
644        let list = ul().child(Node::fragment([
645            Node::fragment([li().text("a").into()]),
646            Node::fragment([]),
647            li().text("b").into(),
648        ]));
649
650        assert_eq!(
651            list.render_pretty(),
652            "<ul>\n  <li>a</li>\n  <li>b</li>\n</ul>"
653        );
654    }
655
656    /// Ports `FragmentTests.testRawHTMLStillEmitsMarkupVerbatim`.
657    #[test]
658    fn raw_markup_is_emitted_verbatim() {
659        let raw = Node::raw(r#"<custom-element data-x="1"></custom-element>"#);
660
661        assert_eq!(
662            raw.render(),
663            r#"<custom-element data-x="1"></custom-element>"#
664        );
665    }
666
667    /// Ports `PrettyPrintTests.testPrettyPrintSimpleTag`.
668    #[test]
669    fn a_tag_with_only_content_stays_on_one_line_when_pretty() {
670        assert_eq!(
671            div().text("Hello World").render_pretty(),
672            "<div>Hello World</div>"
673        );
674    }
675
676    /// Ports `PrettyPrintTests.testPrettyPrintWithChildren`.
677    #[test]
678    fn children_each_get_their_own_line_when_pretty() {
679        let tree = div()
680            .child(p().text("Paragraph 1"))
681            .child(p().text("Paragraph 2"));
682
683        assert_eq!(
684            tree.render_pretty(),
685            "<div>\n  <p>Paragraph 1</p>\n  <p>Paragraph 2</p>\n</div>"
686        );
687    }
688
689    /// Ports `PrettyPrintTests.testCompactRenderStillWorks`.
690    #[test]
691    fn compact_keeps_everything_on_one_line() {
692        assert_eq!(
693            div().child(p().text("Test")).render(),
694            "<div><p>Test</p></div>"
695        );
696    }
697
698    /// Ports `PrettyPrintTests.testSelfClosingTagPrettyPrint`.
699    #[test]
700    fn a_void_element_does_not_self_close_when_pretty() {
701        let rendered = img()
702            .attr("src", "test.jpg")
703            .attr("alt", "Test")
704            .render_pretty();
705
706        assert!(rendered.starts_with("<img"));
707        assert!(!rendered.contains("/>"));
708        assert!(rendered.ends_with('>'));
709    }
710
711    /// Ports `WhitespaceTests.testTextareaKeepsItsContentIntact`.
712    #[test]
713    fn a_textarea_keeps_its_newlines() {
714        let field = textarea().attr("name", "bio").text("line 1\nline 2");
715
716        assert_eq!(
717            field.render_pretty(),
718            "<textarea name=\"bio\">line 1\nline 2</textarea>"
719        );
720    }
721
722    /// Ports `WhitespaceTests.testNestedInsideAPrettyDocumentKeepsOuterIndentation`.
723    #[test]
724    fn a_pre_block_is_indented_from_the_outside_but_not_within() {
725        let container = div().child(pre().child(code().text("swift build")));
726
727        assert_eq!(
728            container.render_pretty(),
729            "<div>\n  <pre><code>swift build</code></pre>\n</div>"
730        );
731    }
732}