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()) || element.is_self_closing() {
292 write_void_suffix(out, options);
293 return;
294 }
295
296 out.push('>');
297 if let Some(content) = element.content() {
298 out.push_str(content);
299 }
300 stack.push(Step::Close(element.tag()));
301 stack.extend(element.children().iter().rev().map(Step::Compact));
302}
303
304fn push_pretty_element<'a>(
306 element: &'a Element,
307 depth: usize,
308 out: &mut String,
309 options: &RenderOptions,
310 stack: &mut Vec<Step<'a>>,
311) {
312 if is_whitespace_sensitive(element.tag()) {
315 options.write_indent(out, depth);
316 stack.push(Step::CompactElement(element));
317 return;
318 }
319
320 options.write_indent(out, depth);
321 out.push('<');
322 out.push_str(element.tag());
323 write_attributes(element.attributes(), out);
324
325 if is_void(element.tag()) || element.is_self_closing() {
326 write_void_suffix(out, options);
327 return;
328 }
329
330 out.push('>');
331
332 if element.children().is_empty() {
333 if let Some(content) = element.content() {
334 out.push_str(content);
335 }
336 out.push_str("</");
337 out.push_str(element.tag());
338 out.push('>');
339 return;
340 }
341
342 if let Some(content) = element.content() {
343 out.push('\n');
344 options.write_indent(out, depth + 1);
345 out.push_str(content);
346 }
347
348 stack.push(Step::ClosePretty(element.tag(), depth));
349 stack.extend(
350 element
351 .children()
352 .iter()
353 .rev()
354 .map(|child| Step::PrettyChild {
355 node: child,
356 depth: depth + 1,
357 group_start: None,
358 }),
359 );
360}
361
362fn write_comment(out: &mut String, content: &str) {
363 out.push_str("<!-- ");
364 out.push_str(content);
365 out.push_str(" -->");
366}
367
368fn write_attributes(attributes: &[Attribute], out: &mut String) {
369 for attribute in attributes {
370 attribute.write_into(out);
371 }
372}
373
374fn write_void_suffix(out: &mut String, options: &RenderOptions) {
376 out.push_str(if options.xhtml_self_closing {
377 " />"
378 } else {
379 ">"
380 });
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use crate::elements::{
387 body, code, div, h1, head, html_tag, i, img, li, p, pre, span, textarea, title, ul,
388 };
389 use crate::html;
392
393 #[test]
394 fn the_tree_is_send_and_sync() {
395 fn assert_send_sync<T: Send + Sync>() {}
396 assert_send_sync::<Node>();
397 assert_send_sync::<Element>();
398 }
399
400 #[test]
401 fn text_nodes_are_escaped() {
402 assert_eq!(Node::text("a & b").render(), "a & b");
403 }
404
405 #[test]
409 fn raw_nodes_are_never_escaped() {
410 assert_eq!(Node::raw("<b>bold</b>").render(), "<b>bold</b>");
411 assert_eq!(Node::raw("<b>bold</b>").render_pretty(), "<b>bold</b>");
412 }
413
414 #[test]
415 fn a_comment_cannot_close_itself_early() {
416 let rendered = Node::comment("a --> b").render();
417 assert_eq!(rendered.matches("-->").count(), 1);
418 assert!(rendered.ends_with("-->"));
419 }
420
421 #[test]
423 fn an_empty_fragment_leaves_no_blank_line() {
424 let tree = div()
425 .child(p().text("a"))
426 .child(Node::fragment([]))
427 .child(p().text("b"));
428 assert_eq!(
429 tree.render_pretty(),
430 "<div>\n <p>a</p>\n <p>b</p>\n</div>"
431 );
432 }
433
434 #[test]
436 fn a_fragment_renders_its_children_without_a_wrapper() {
437 let tree = Node::fragment([p().text("a").into(), p().text("b").into()]);
438 assert_eq!(tree.render(), "<p>a</p><p>b</p>");
439 assert_eq!(tree.render_pretty(), "<p>a</p>\n<p>b</p>");
440 }
441
442 #[test]
444 fn void_elements_take_no_children() {
445 let tree = img().attr("src", "/a.png").child(span().text("dropped"));
446 assert_eq!(tree.render(), r#"<img src="/a.png">"#);
447 }
448
449 #[test]
450 fn xhtml_mode_closes_void_elements_with_a_slash() {
451 let options = RenderOptions::compact().with_xhtml_self_closing(true);
452 assert_eq!(
453 img().attr("src", "/a.png").render_with(&options),
454 r#"<img src="/a.png" />"#
455 );
456 }
457
458 #[test]
461 fn whitespace_sensitive_tags_are_not_indented_inside() {
462 let tree = div().child(pre().child(code().text("let page = html { }")));
463 assert_eq!(
464 tree.render_pretty(),
465 "<div>\n <pre><code>let page = html { }</code></pre>\n</div>"
466 );
467 }
468
469 #[test]
471 fn content_goes_on_its_own_line_before_children() {
472 let tree = div().text("lead").child(p().text("body"));
473 assert_eq!(tree.render_pretty(), "<div>\n lead\n <p>body</p>\n</div>");
474 }
475
476 #[test]
477 fn an_element_with_content_and_no_children_stays_on_one_line() {
478 assert_eq!(p().text("hi").render_pretty(), "<p>hi</p>");
479 }
480
481 #[test]
482 fn compact_is_the_default_for_an_element() {
483 let tree = div().child(p().text("a"));
484 assert_eq!(tree.render(), "<div><p>a</p></div>");
485 }
486
487 #[test]
492 fn raw_markup_renders_with_no_wrapper_around_it() {
493 let raw = Node::raw(r#"<span class="x">hi</span>"#);
494
495 assert_eq!(raw.render(), r#"<span class="x">hi</span>"#);
496 assert!(!raw.render().contains("<div"));
497 }
498
499 #[test]
501 fn raw_markup_as_a_child_adds_no_wrapper() {
502 let markup = div()
503 .child(Node::raw(r#"<i class="fa fa-home"></i>"#))
504 .child(span().text("Home"));
505
506 assert_eq!(
507 markup.render(),
508 r#"<div><i class="fa fa-home"></i><span>Home</span></div>"#
509 );
510 }
511
512 #[test]
514 fn a_fragment_renders_its_children_with_no_wrapper() {
515 let fragment = Node::fragment([
516 i().add_class("fa fa-star").into(),
517 span().text(" Featured").into(),
518 ]);
519
520 assert_eq!(
521 fragment.render(),
522 r#"<i class="fa fa-star"></i><span> Featured</span>"#
523 );
524 }
525
526 #[test]
528 fn a_fragment_takes_a_mapped_sequence() {
529 let fragment = Node::fragment(
530 ["One", "Two", "Three"].map(|title| div().add_class("card").text(title).into()),
531 );
532
533 assert_eq!(
534 fragment.render(),
535 concat!(
536 r#"<div class="card">One</div><div class="card">Two</div>"#,
537 r#"<div class="card">Three</div>"#,
538 )
539 );
540 }
541
542 #[test]
548 fn an_empty_fragment_renders_nothing_in_either_mode() {
549 assert_eq!(Node::fragment([]).render(), "");
550 assert_eq!(Node::fragment([]).render_pretty(), "");
551 }
552
553 #[test]
555 fn a_fragments_children_are_indented_as_the_parents_own() {
556 let list = ul().child(Node::fragment([
557 li().text("a").into(),
558 li().text("b").into(),
559 ]));
560
561 assert_eq!(
562 list.render_pretty(),
563 "<ul>\n <li>a</li>\n <li>b</li>\n</ul>"
564 );
565 }
566
567 #[test]
569 fn a_fragment_takes_a_filtered_and_mapped_sequence() {
570 let names = ["Ana", "Bruno", "Carla"];
571 let group = Node::fragment(
572 names
573 .iter()
574 .filter(|name| name.len() > 3)
575 .map(|name| li().text(name).into()),
576 );
577
578 assert_eq!(group.render(), "<li>Bruno</li><li>Carla</li>");
579 }
580
581 #[test]
583 fn a_false_condition_emits_no_stray_node() {
584 let show_banner = false;
585 let page = html_tag()
586 .child(head().child(title().text("Home")))
587 .child(html! { @if show_banner { div { "banner" } } })
588 .child(body().child(h1().text("Hi")));
589
590 assert_eq!(
591 page.render(),
592 "<html><head><title>Home</title></head><body><h1>Hi</h1></body></html>"
593 );
594 }
595
596 #[test]
598 fn a_true_condition_emits_its_branch() {
599 let show_banner = true;
600 let page = html_tag().child(html! { @if show_banner { div { "banner" } } });
601
602 assert_eq!(page.render(), "<html><div>banner</div></html>");
603 }
604
605 #[test]
607 fn a_loop_emits_one_node_per_iteration() {
608 let page = html_tag().child(html! {
609 @for index in 1..=3 { p { "line " (index) } }
610 });
611
612 assert_eq!(
613 page.render(),
614 "<html><p>line 1</p><p>line 2</p><p>line 3</p></html>"
615 );
616 }
617
618 #[test]
620 fn raw_markup_is_indented_as_one_blob() {
621 let container = div().child(Node::raw("<custom-element></custom-element>"));
622
623 assert_eq!(
624 container.render_pretty(),
625 "<div>\n <custom-element></custom-element>\n</div>"
626 );
627 }
628
629 #[test]
631 fn a_condition_can_pick_either_branch() {
632 fn badge(is_beta: bool) -> Node {
633 html! { @if is_beta { span { "beta" } } @else { span { "stable" } } }
634 }
635
636 assert_eq!(badge(true).render(), "<span>beta</span>");
637 assert_eq!(badge(false).render(), "<span>stable</span>");
638 }
639
640 #[test]
645 fn nested_fragments_flatten_without_leaving_gaps() {
646 let list = ul().child(Node::fragment([
647 Node::fragment([li().text("a").into()]),
648 Node::fragment([]),
649 li().text("b").into(),
650 ]));
651
652 assert_eq!(
653 list.render_pretty(),
654 "<ul>\n <li>a</li>\n <li>b</li>\n</ul>"
655 );
656 }
657
658 #[test]
660 fn raw_markup_is_emitted_verbatim() {
661 let raw = Node::raw(r#"<custom-element data-x="1"></custom-element>"#);
662
663 assert_eq!(
664 raw.render(),
665 r#"<custom-element data-x="1"></custom-element>"#
666 );
667 }
668
669 #[test]
671 fn a_tag_with_only_content_stays_on_one_line_when_pretty() {
672 assert_eq!(
673 div().text("Hello World").render_pretty(),
674 "<div>Hello World</div>"
675 );
676 }
677
678 #[test]
680 fn children_each_get_their_own_line_when_pretty() {
681 let tree = div()
682 .child(p().text("Paragraph 1"))
683 .child(p().text("Paragraph 2"));
684
685 assert_eq!(
686 tree.render_pretty(),
687 "<div>\n <p>Paragraph 1</p>\n <p>Paragraph 2</p>\n</div>"
688 );
689 }
690
691 #[test]
693 fn compact_keeps_everything_on_one_line() {
694 assert_eq!(
695 div().child(p().text("Test")).render(),
696 "<div><p>Test</p></div>"
697 );
698 }
699
700 #[test]
702 fn a_void_element_does_not_self_close_when_pretty() {
703 let rendered = img()
704 .attr("src", "test.jpg")
705 .attr("alt", "Test")
706 .render_pretty();
707
708 assert!(rendered.starts_with("<img"));
709 assert!(!rendered.contains("/>"));
710 assert!(rendered.ends_with('>'));
711 }
712
713 #[test]
715 fn a_textarea_keeps_its_newlines() {
716 let field = textarea().attr("name", "bio").text("line 1\nline 2");
717
718 assert_eq!(
719 field.render_pretty(),
720 "<textarea name=\"bio\">line 1\nline 2</textarea>"
721 );
722 }
723
724 #[test]
726 fn a_pre_block_is_indented_from_the_outside_but_not_within() {
727 let container = div().child(pre().child(code().text("swift build")));
728
729 assert_eq!(
730 container.render_pretty(),
731 "<div>\n <pre><code>swift build</code></pre>\n</div>"
732 );
733 }
734}