1use 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#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Node {
27 Element(Element),
29 Text(String),
31 Raw(String),
36 Comment(String),
40 Fragment(Vec<Node>),
45}
46
47impl Node {
48 #[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 #[must_use]
59 pub fn raw(content: impl Into<String>) -> Self {
60 Self::Raw(content.into())
61 }
62
63 #[must_use]
74 pub fn comment(content: impl AsRef<str>) -> Self {
75 Self::Comment(content.as_ref().replace("--", "- -"))
76 }
77
78 #[must_use]
80 pub fn fragment(children: impl IntoIterator<Item = Node>) -> Self {
81 Self::Fragment(children.into_iter().collect())
82 }
83
84 #[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
132pub(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
154enum Step<'a> {
162 Compact(&'a Node),
164 Pretty(&'a Node, usize),
166 CompactElement(&'a Element),
168 PrettyElement(&'a Element, usize),
170 Close(&'a str),
172 ClosePretty(&'a str, usize),
174 PrettyChild {
181 node: &'a Node,
182 depth: usize,
183 group_start: Option<usize>,
184 },
185 DropIfEmpty { mark: usize, after: usize },
192}
193
194fn 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 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 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 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
276fn 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 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
302fn 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 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
372fn 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 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 & b");
401 }
402
403 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}