1use std::borrow::Cow;
7
8use crate::core::attribute::Attribute;
9use crate::core::escape::write_escaped_text;
10use crate::core::node::{Node, Source};
11use crate::core::render::{Render, RenderOptions};
12
13#[derive(Debug)]
32pub struct Element {
33 tag: Cow<'static, str>,
36 attributes: Vec<Attribute>,
37 content: Option<String>,
38 children: Vec<Node>,
39 self_closing: bool,
40}
41
42impl Clone for Element {
43 fn clone(&self) -> Self {
44 match clone_tree(Source::Element(self)) {
45 Node::Element(element) => element,
46 _ => unreachable!("cloning an element yields an element"),
47 }
48 }
49}
50
51impl PartialEq for Element {
52 fn eq(&self, other: &Self) -> bool {
53 trees_equal(Source::Element(self), Source::Element(other))
54 }
55}
56
57impl Eq for Element {}
58
59impl std::fmt::Display for Element {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(&if f.alternate() {
63 self.render_pretty()
64 } else {
65 self.render()
66 })
67 }
68}
69
70impl std::hash::Hash for Element {
71 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
72 hash_tree(Source::Element(self), state);
73 }
74}
75
76impl<N: Into<Node>> Extend<N> for Element {
78 fn extend<I: IntoIterator<Item = N>>(&mut self, iter: I) {
79 self.children.extend(iter.into_iter().map(Into::into));
80 }
81}
82
83pub(crate) fn hash_tree<H: std::hash::Hasher>(root: Source<'_>, state: &mut H) {
86 use std::hash::Hash;
87
88 let mut pending = vec![root];
89 while let Some(source) = pending.pop() {
90 match source {
91 Source::Element(element) | Source::Node(Node::Element(element)) => {
92 0_u8.hash(state);
93 element.tag.hash(state);
94 element.attributes.hash(state);
95 element.content.hash(state);
96 element.self_closing.hash(state);
97 element.children.len().hash(state);
98 pending.extend(element.children.iter().rev().map(Source::Node));
99 }
100 Source::Node(Node::Fragment(children)) => {
101 1_u8.hash(state);
102 children.len().hash(state);
103 pending.extend(children.iter().rev().map(Source::Node));
104 }
105 Source::Node(Node::Text(text)) => {
106 2_u8.hash(state);
107 text.hash(state);
108 }
109 Source::Node(Node::Raw(text)) => {
110 3_u8.hash(state);
111 text.hash(state);
112 }
113 Source::Node(Node::Comment(text)) => {
114 4_u8.hash(state);
115 text.hash(state);
116 }
117 }
118 }
119}
120
121fn shell(element: &Element) -> Element {
123 Element {
124 tag: element.tag.clone(),
125 attributes: element.attributes.clone(),
126 content: element.content.clone(),
127 children: Vec::with_capacity(element.children.len()),
128 self_closing: element.self_closing,
129 }
130}
131
132enum CloneFrame<'a> {
134 Element {
135 source: &'a [Node],
136 built: Element,
137 },
138 Fragment {
139 source: &'a [Node],
140 built: Vec<Node>,
141 },
142}
143
144pub(crate) fn clone_tree(root: Source<'_>) -> Node {
146 let mut stack: Vec<CloneFrame<'_>> = Vec::new();
147 let mut descend = Some(root);
148
149 loop {
150 let mut finished = None;
152 if let Some(source) = descend.take() {
153 match source {
154 Source::Element(element) | Source::Node(Node::Element(element)) => {
155 stack.push(CloneFrame::Element {
156 source: &element.children,
157 built: shell(element),
158 });
159 }
160 Source::Node(Node::Fragment(children)) => stack.push(CloneFrame::Fragment {
161 source: children,
162 built: Vec::with_capacity(children.len()),
163 }),
164 Source::Node(Node::Text(text)) => finished = Some(Node::Text(text.clone())),
165 Source::Node(Node::Raw(text)) => finished = Some(Node::Raw(text.clone())),
166 Source::Node(Node::Comment(text)) => finished = Some(Node::Comment(text.clone())),
167 }
168 }
169
170 if finished.is_none() {
171 let Some(frame) = stack.last_mut() else {
173 return Node::Fragment(Vec::new());
175 };
176 let (source, built_len) = match frame {
177 CloneFrame::Element { source, built } => (*source, built.children.len()),
178 CloneFrame::Fragment { source, built } => (*source, built.len()),
179 };
180 if let Some(next) = source.get(built_len) {
181 descend = Some(Source::Node(next));
182 continue;
183 }
184 finished = stack.pop().map(|frame| match frame {
185 CloneFrame::Element { built, .. } => Node::Element(built),
186 CloneFrame::Fragment { built, .. } => Node::Fragment(built),
187 });
188 }
189
190 let Some(done) = finished else { continue };
191 match stack.last_mut() {
192 None => return done,
193 Some(CloneFrame::Element { built, .. }) => built.children.push(done),
194 Some(CloneFrame::Fragment { built, .. }) => built.push(done),
195 }
196 }
197}
198
199pub(crate) fn trees_equal(left: Source<'_>, right: Source<'_>) -> bool {
201 let mut pending = vec![(left, right)];
202 while let Some((left, right)) = pending.pop() {
203 match (left, right) {
204 (
205 Source::Element(a) | Source::Node(Node::Element(a)),
206 Source::Element(b) | Source::Node(Node::Element(b)),
207 ) => {
208 if a.tag != b.tag
209 || a.attributes != b.attributes
210 || a.content != b.content
211 || a.self_closing != b.self_closing
212 || a.children.len() != b.children.len()
213 {
214 return false;
215 }
216 pending.extend(
217 a.children
218 .iter()
219 .zip(&b.children)
220 .map(|(a, b)| (Source::Node(a), Source::Node(b))),
221 );
222 }
223 (Source::Node(Node::Fragment(a)), Source::Node(Node::Fragment(b))) => {
224 if a.len() != b.len() {
225 return false;
226 }
227 pending.extend(
228 a.iter()
229 .zip(b)
230 .map(|(a, b)| (Source::Node(a), Source::Node(b))),
231 );
232 }
233 (Source::Node(Node::Text(a)), Source::Node(Node::Text(b)))
234 | (Source::Node(Node::Raw(a)), Source::Node(Node::Raw(b)))
235 | (Source::Node(Node::Comment(a)), Source::Node(Node::Comment(b))) => {
236 if a != b {
237 return false;
238 }
239 }
240 _ => return false,
241 }
242 }
243 true
244}
245
246impl Element {
247 pub fn new(tag: impl Into<String>) -> Self {
249 Self::with_tag(Cow::Owned(tag.into()))
250 }
251
252 #[must_use]
263 pub fn from_static(tag: &'static str) -> Self {
264 Self::with_tag(Cow::Borrowed(tag))
265 }
266
267 fn with_tag(tag: Cow<'static, str>) -> Self {
268 Self {
269 tag,
270 attributes: Vec::new(),
271 content: None,
272 children: Vec::new(),
273 self_closing: false,
274 }
275 }
276
277 #[must_use]
281 pub fn tag(&self) -> &str {
282 &self.tag
283 }
284
285 #[must_use]
287 pub fn attributes(&self) -> &[Attribute] {
288 &self.attributes
289 }
290
291 #[must_use]
293 pub fn content(&self) -> Option<&str> {
294 self.content.as_deref()
295 }
296
297 #[must_use]
299 pub fn children(&self) -> &[Node] {
300 &self.children
301 }
302
303 #[must_use]
315 pub fn is_self_closing(&self) -> bool {
316 self.self_closing
317 }
318
319 #[must_use]
325 pub fn text(mut self, content: impl AsRef<str>) -> Self {
326 let raw = content.as_ref();
327 let mut escaped = String::with_capacity(raw.len());
328 write_escaped_text(&mut escaped, raw, false);
329 self.content = Some(escaped);
330 self
331 }
332
333 #[must_use]
344 pub fn raw_text(mut self, content: impl Into<String>) -> Self {
345 self.content = Some(content.into());
346 self
347 }
348
349 #[must_use]
351 pub fn child(mut self, child: impl Into<Node>) -> Self {
352 self.children.push(child.into());
353 self
354 }
355
356 #[must_use]
358 pub fn children_from<N: Into<Node>>(mut self, children: impl IntoIterator<Item = N>) -> Self {
359 self.children.extend(children.into_iter().map(Into::into));
360 self
361 }
362
363 #[must_use]
367 pub fn add_attribute(mut self, attribute: Attribute) -> Self {
368 self.attributes.push(attribute);
369 self
370 }
371
372 #[must_use]
375 pub fn attr(self, key: impl Into<String>, value: impl AsRef<str>) -> Self {
376 self.add_attribute(Attribute::new(key, value))
377 }
378
379 #[must_use]
381 pub fn bool_attr(self, key: impl Into<String>) -> Self {
382 self.add_attribute(Attribute::boolean(key))
383 }
384
385 #[must_use]
400 pub fn trusted_url(mut self, key: impl Into<String>, value: impl AsRef<str>) -> Self {
401 let attribute = Attribute::trusted_url(key, value);
402 self.attributes
405 .retain(|existing| existing.key() != attribute.key());
406 self.attributes.push(attribute);
407 self
408 }
409
410 #[must_use]
429 pub fn self_closing(mut self, yes: bool) -> Self {
430 self.self_closing = yes;
431 self
432 }
433
434 #[must_use]
436 pub fn data_attr(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
437 self.attr(format!("data-{}", key.as_ref()), value)
438 }
439
440 #[must_use]
446 pub fn data_attrs<K: AsRef<str>, V: AsRef<str>>(
447 mut self,
448 data: impl IntoIterator<Item = (K, V)>,
449 ) -> Self {
450 for (key, value) in data {
451 self = self.data_attr(key, value);
452 }
453 self
454 }
455
456 #[must_use]
458 pub fn aria_attr(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
459 self.attr(format!("aria-{}", key.as_ref()), value)
460 }
461
462 #[must_use]
464 pub fn aria_attrs<K: AsRef<str>, V: AsRef<str>>(
465 mut self,
466 aria: impl IntoIterator<Item = (K, V)>,
467 ) -> Self {
468 for (key, value) in aria {
469 self = self.aria_attr(key, value);
470 }
471 self
472 }
473
474 #[must_use]
481 pub fn set_id(self, id: impl AsRef<str>) -> Self {
482 self.replace_attribute("id", id.as_ref())
483 }
484
485 #[must_use]
487 pub fn set_style(self, style: impl AsRef<str>) -> Self {
488 self.replace_attribute("style", style.as_ref())
489 }
490
491 #[must_use]
493 pub fn set_role(self, role: impl AsRef<str>) -> Self {
494 self.replace_attribute("role", role.as_ref())
495 }
496
497 fn replace_attribute(mut self, key: &'static str, value: &str) -> Self {
498 self.attributes.retain(|a| a.key() != key);
499 self.attributes.push(Attribute::new_static(key, value));
500 self
501 }
502
503 #[must_use]
510 pub fn add_class(mut self, class_name: impl AsRef<str>) -> Self {
511 let raw = class_name.as_ref();
512 if let Some(existing) = self.attributes.iter_mut().find(|a| a.key() == "class") {
513 existing.push_escaped_word(raw);
514 } else {
515 self.attributes.push(Attribute::new_static("class", raw));
516 }
517 self
518 }
519
520 #[must_use]
522 pub fn add_classes<S: AsRef<str>>(mut self, class_names: impl IntoIterator<Item = S>) -> Self {
523 for name in class_names {
524 self = self.add_class(name);
525 }
526 self
527 }
528}
529
530impl Render for Element {
531 fn write_into(&self, out: &mut String, options: &RenderOptions, depth: usize) {
532 crate::core::node::write_element_tree(self, out, options, depth);
533 }
534}
535
536impl Drop for Element {
545 fn drop(&mut self) {
546 let mut pending = core::mem::take(&mut self.children);
547 while let Some(node) = pending.pop() {
548 match node {
549 Node::Element(mut element) => pending.append(&mut element.children),
550 Node::Fragment(mut children) => pending.append(&mut children),
551 Node::Text(_) | Node::Raw(_) | Node::Comment(_) => {}
552 }
553 }
554 }
555}
556
557#[cfg(test)]
558mod tests {
559
560 #[test]
562 fn a_clone_equals_its_source_and_is_independent() {
563 let source = div()
564 .attr("id", "a")
565 .add_class("x y")
566 .child(p().text("t").child(Node::comment("c")))
567 .child(Node::fragment([
568 Node::text("a"),
569 Node::raw("<b>"),
570 Node::fragment([]),
571 span().into(),
572 ]))
573 .child(Node::text("tail"));
574 let copy = source.clone();
575 assert_eq!(source, copy);
576 assert_eq!(source.render(), copy.render());
577
578 let changed = copy.clone().child(Node::text("more"));
579 assert_ne!(source, changed);
580 assert_eq!(source.render(), copy.render(), "the original is untouched");
581 }
582
583 #[test]
584 fn equality_sees_every_field() {
585 let base = div().attr("id", "a").text("t");
586 assert_eq!(base, div().attr("id", "a").text("t"));
587 assert_ne!(base, div().attr("id", "b").text("t"));
588 assert_ne!(base, div().attr("id", "a").text("u"));
589 assert_ne!(base, span().attr("id", "a").text("t"));
590 assert_ne!(base, div().attr("id", "a").text("t").self_closing(true));
591 assert_ne!(Node::text("a"), Node::raw("a"));
592 assert_ne!(Node::text("a"), Node::comment("a"));
593 assert_ne!(Node::fragment([]), Node::fragment([Node::text("")]));
594 assert_eq!(Node::from(base.clone()), Node::from(base));
595 }
596
597 use super::*;
598 use crate::core::RenderOptions;
599 use crate::elements::{
600 a, body, button, div, footer, head, header, html_tag, img, input_named, li, main_tag, meta,
601 nav, ol, p, script, span, stylesheet, table, td, th, title, tr, ul,
602 };
603
604 #[test]
606 fn trusted_url_replaces_a_blocked_value() {
607 assert_eq!(
608 a().attr("href", "javascript:x")
609 .trusted_url("href", "javascript:print()")
610 .render(),
611 r#"<a href="javascript:print()"></a>"#
612 );
613 }
614
615 #[test]
616 fn a_non_void_tag_marked_self_closing_drops_its_children() {
617 let tag = div()
618 .attr("id", "x")
619 .self_closing(true)
620 .child(span().text("no"));
621 assert_eq!(tag.render(), r#"<div id="x">"#);
622 assert_eq!(
623 tag.render_with(&RenderOptions::compact().with_xhtml_self_closing(true)),
624 r#"<div id="x" />"#
625 );
626 assert_eq!(tag.render_pretty(), r#"<div id="x">"#);
627 assert!(!div().is_self_closing());
628 }
629
630 #[test]
631 fn add_class_appends_to_one_attribute() {
632 assert_eq!(
633 div().add_class("a").add_class("b").render(),
634 r#"<div class="a b"></div>"#
635 );
636 }
637
638 #[test]
639 fn chaining_add_class_does_not_double_escape() {
640 let rendered = div().add_class("a&b").add_class("c").render();
641 assert_eq!(rendered, r#"<div class="a&b c"></div>"#);
642 assert!(!rendered.contains("&amp;"));
643 }
644
645 #[test]
646 fn a_quote_in_a_class_name_cannot_break_out() {
647 let rendered = div().add_class(r#"a" onload="alert(1)"#).render();
648 assert!(!rendered.contains("onload=\""));
649 assert!(rendered.contains("""));
650 }
651
652 #[test]
653 fn add_classes_appends_all_of_them_in_order() {
654 assert_eq!(
655 div().add_classes(["card", "p-4", "shadow"]).render(),
656 r#"<div class="card p-4 shadow"></div>"#
657 );
658 }
659
660 #[test]
661 fn set_id_replaces_rather_than_appending() {
662 let rendered = div().set_id("first").set_id("second").render();
663 assert_eq!(rendered, r#"<div id="second"></div>"#);
664 }
665
666 #[test]
667 fn set_style_and_set_role_also_replace() {
668 let rendered = div()
669 .set_style("color:red")
670 .set_style("color:blue")
671 .set_role("main")
672 .render();
673 assert_eq!(rendered, r#"<div style="color:blue" role="main"></div>"#);
674 }
675
676 #[test]
677 fn data_and_aria_attributes_get_their_prefixes() {
678 let rendered = span()
679 .data_attr("id", "7")
680 .aria_attr("label", "Close")
681 .render();
682 assert_eq!(rendered, r#"<span data-id="7" aria-label="Close"></span>"#);
683 }
684
685 #[test]
687 fn bulk_attribute_order_is_stable() {
688 let build = || {
689 div()
690 .data_attrs([("a", "1"), ("b", "2"), ("c", "3")])
691 .render()
692 };
693 let expected = r#"<div data-a="1" data-b="2" data-c="3"></div>"#;
694 for _ in 0..16 {
695 assert_eq!(build(), expected);
696 }
697 }
698
699 #[test]
702 fn plain_attributes_are_appended_without_deduplication() {
703 assert_eq!(
704 div().attr("data-x", "1").attr("data-x", "2").render(),
705 r#"<div data-x="1" data-x="2"></div>"#
706 );
707 }
708
709 #[test]
712 fn text_is_escaped_and_raw_text_is_not() {
713 assert_eq!(p().text("<b>").render(), "<p><b></p>");
714 assert_eq!(p().raw_text("<b>").render(), "<p><b></p>");
715 }
716
717 #[test]
718 fn boolean_attributes_render_bare() {
719 assert_eq!(
720 button().bool_attr("disabled").render(),
721 "<button disabled></button>"
722 );
723 }
724
725 #[test]
726 fn children_from_appends_a_sequence() {
727 let list = div().children_from([p().text("a"), p().text("b")]);
728 assert_eq!(list.render(), "<div><p>a</p><p>b</p></div>");
729 }
730
731 #[test]
732 fn a_tag_renders_its_attributes_then_its_content() {
733 let tag = Element::new("p")
734 .attr("class", "text")
735 .text("Hello, World!");
736
737 assert_eq!(tag.render(), r#"<p class="text">Hello, World!</p>"#);
738 }
739
740 #[test]
743 fn extra_attributes_can_precede_the_typed_ones() {
744 let tag = img()
745 .attr("width", "100")
746 .attr("height", "100")
747 .attr("src", "image.png")
748 .attr("alt", "An image");
749
750 assert_eq!(
751 tag.render(),
752 r#"<img width="100" height="100" src="image.png" alt="An image">"#
753 );
754 }
755
756 #[test]
757 fn nested_children_render_in_order() {
758 let document = html_tag().child(
759 div()
760 .child(p().text("This is a paragraph."))
761 .child(img().attr("src", "image.png").attr("alt", "An image")),
762 );
763
764 assert_eq!(
765 document.render(),
766 concat!(
767 "<html><div><p>This is a paragraph.</p>",
768 r#"<img src="image.png" alt="An image"></div></html>"#,
769 )
770 );
771 }
772
773 #[test]
774 fn a_container_renders_its_class_before_its_children() {
775 let document = html_tag().child(
776 div()
777 .add_class("main-body")
778 .child(p().text("Title"))
779 .child(p().text("This is a paragraph.")),
780 );
781
782 assert_eq!(
783 document.render(),
784 r#"<html><div class="main-body"><p>Title</p><p>This is a paragraph.</p></div></html>"#
785 );
786 }
787
788 #[test]
789 fn a_table_renders_its_rows_and_cells() {
790 let document = html_tag().child(
791 table()
792 .add_class("table")
793 .child(
794 tr().child(th().text("Header 1"))
795 .child(th().text("Header 2")),
796 )
797 .child(
798 tr().child(td().text("Row 1, Cell 1"))
799 .child(td().text("Row 1, Cell 2")),
800 )
801 .child(
802 tr().child(td().text("Row 2, Cell 1"))
803 .child(td().text("Row 2, Cell 2")),
804 ),
805 );
806
807 assert_eq!(
808 document.render(),
809 concat!(
810 r#"<html><table class="table"><tr><th>Header 1</th><th>Header 2</th></tr>"#,
811 "<tr><td>Row 1, Cell 1</td><td>Row 1, Cell 2</td></tr>",
812 "<tr><td>Row 2, Cell 1</td><td>Row 2, Cell 2</td></tr></table></html>",
813 )
814 );
815 }
816
817 #[test]
818 fn an_unordered_list_renders_its_items() {
819 let document = html_tag().child(
820 ul().add_class("unordered-list")
821 .child(li().text("Item 1"))
822 .child(li().text("Item 2"))
823 .child(li().text("Item 3")),
824 );
825
826 assert_eq!(
827 document.render(),
828 concat!(
829 r#"<html><ul class="unordered-list">"#,
830 "<li>Item 1</li><li>Item 2</li><li>Item 3</li></ul></html>",
831 )
832 );
833 }
834
835 #[test]
836 fn an_ordered_list_renders_its_items() {
837 let document = html_tag().child(
838 ol().add_class("ordered-list")
839 .child(li().text("First"))
840 .child(li().text("Second"))
841 .child(li().text("Third")),
842 );
843
844 assert_eq!(
845 document.render(),
846 concat!(
847 r#"<html><ol class="ordered-list">"#,
848 "<li>First</li><li>Second</li><li>Third</li></ol></html>",
849 )
850 );
851 }
852
853 #[test]
856 fn a_description_list_renders_terms_and_descriptions() {
857 let document = html_tag().child(
858 Element::new("dl")
859 .add_class("description-list")
860 .child(Element::new("dt").text("Term 1"))
861 .child(Element::new("dd").text("Description 1"))
862 .child(Element::new("dt").text("Term 2"))
863 .child(Element::new("dd").text("Description 2")),
864 );
865
866 assert_eq!(
867 document.render(),
868 concat!(
869 r#"<html><dl class="description-list">"#,
870 "<dt>Term 1</dt><dd>Description 1</dd>",
871 "<dt>Term 2</dt><dd>Description 2</dd></dl></html>",
872 )
873 );
874 }
875
876 #[test]
877 fn the_structural_tags_nest_into_a_page() {
878 let document = html_tag()
879 .child(
880 head()
881 .child(
882 meta()
883 .attr("name", "description")
884 .attr("content", "A description of the page"),
885 )
886 .child(stylesheet("styles.css")),
887 )
888 .child(
889 body()
890 .child(
891 header().child(
892 nav()
893 .child(a().attr("href", "#home").text("Home"))
894 .child(a().attr("href", "#about").text("About"))
895 .child(a().attr("href", "#contact").text("Contact")),
896 ),
897 )
898 .child(main_tag().child(p().text("Welcome to our website!")))
899 .child(footer().child(p().text("\u{a9} 2024 Company, Inc."))),
900 );
901
902 assert_eq!(
903 document.render(),
904 concat!(
905 r#"<html><head><meta name="description" content="A description of the page">"#,
906 r#"<link href="styles.css" rel="stylesheet"></head><body><header><nav>"#,
907 r##"<a href="#home">Home</a><a href="#about">About</a>"##,
908 r##"<a href="#contact">Contact</a></nav></header>"##,
909 "<main><p>Welcome to our website!</p></main>",
910 "<footer><p>\u{a9} 2024 Company, Inc.</p></footer></body></html>",
911 )
912 );
913 }
914
915 #[test]
918 fn a_script_body_is_not_escaped() {
919 let document = html_tag().child(
920 script()
921 .attr("type", "text/javascript")
922 .raw_text("alert('Hello World');"),
923 );
924
925 assert_eq!(
926 document.render(),
927 r#"<html><script type="text/javascript">alert('Hello World');</script></html>"#
928 );
929 }
930
931 #[test]
932 fn meta_renders_both_the_named_and_the_charset_form() {
933 let document = html_tag()
934 .child(
935 meta()
936 .attr("name", "description")
937 .attr("content", "A description of the page"),
938 )
939 .child(meta().attr("charset", "utf-8"));
940
941 assert_eq!(
942 document.render(),
943 concat!(
944 r#"<html><meta name="description" content="A description of the page">"#,
945 r#"<meta charset="utf-8"></html>"#,
946 )
947 );
948 }
949
950 #[test]
951 fn a_title_renders_its_text() {
952 let document = html_tag().child(title().text("Title my site"));
953
954 assert_eq!(
955 document.render(),
956 "<html><title>Title my site</title></html>"
957 );
958 }
959
960 #[test]
961 fn a_span_renders_its_attributes_and_content() {
962 let tag = span().attr("class", "text").text("Hello, World!");
963
964 assert_eq!(tag.render(), r#"<span class="text">Hello, World!</span>"#);
965 }
966
967 #[test]
970 fn a_button_carries_its_type_after_its_class() {
971 let document = html_tag().child(button().add_class("button-class").attr("type", "button"));
972
973 assert_eq!(
974 document.render(),
975 r#"<html><button class="button-class" type="button"></button></html>"#
976 );
977 }
978
979 #[test]
980 fn a_button_renders_its_children() {
981 let document = html_tag().child(
982 button()
983 .add_class("button-class")
984 .attr("type", "button")
985 .child(span().add_class("icon-bar")),
986 );
987
988 assert_eq!(
989 document.render(),
990 concat!(
991 r#"<html><button class="button-class" type="button">"#,
992 r#"<span class="icon-bar"></span></button></html>"#,
993 )
994 );
995 }
996
997 #[test]
1000 fn chaining_helpers_keeps_the_element_usable() {
1001 let card: Element = div().add_class("card").set_id("hero").set_role("region");
1002
1003 assert_eq!(
1004 card.render(),
1005 r#"<div class="card" id="hero" role="region"></div>"#
1006 );
1007 }
1008
1009 #[test]
1010 fn set_style_escapes_quotes() {
1011 let rendered = div()
1012 .set_style(r#"font-family: "Inter", sans-serif"#)
1013 .render();
1014
1015 assert_eq!(
1016 rendered,
1017 r#"<div style="font-family: "Inter", sans-serif"></div>"#
1018 );
1019 }
1020
1021 #[test]
1022 fn a_single_class_renders_on_its_own() {
1023 assert_eq!(
1024 div().add_class("container").render(),
1025 r#"<div class="container"></div>"#
1026 );
1027 }
1028
1029 #[test]
1030 fn set_id_renders_an_id_attribute() {
1031 assert_eq!(
1032 div().set_id("main-content").render(),
1033 r#"<div id="main-content"></div>"#
1034 );
1035 }
1036
1037 #[test]
1038 fn the_helpers_chain_in_the_order_they_are_called() {
1039 let rendered = div()
1040 .set_id("content")
1041 .add_class("container")
1042 .add_class("active")
1043 .set_style("padding: 20px;")
1044 .render();
1045
1046 assert_eq!(
1047 rendered,
1048 r#"<div id="content" class="container active" style="padding: 20px;"></div>"#
1049 );
1050 }
1051
1052 #[test]
1055 fn several_data_attributes_keep_the_order_they_are_given() {
1056 let rendered = div()
1057 .data_attrs([("id", "123"), ("type", "product")])
1058 .render();
1059
1060 assert_eq!(rendered, r#"<div data-id="123" data-type="product"></div>"#);
1061 }
1062
1063 #[test]
1064 fn several_aria_attributes_keep_the_order_they_are_given() {
1065 let rendered = nav()
1066 .aria_attrs([("label", "Main navigation"), ("expanded", "true")])
1067 .render();
1068
1069 assert_eq!(
1070 rendered,
1071 r#"<nav aria-label="Main navigation" aria-expanded="true"></nav>"#
1072 );
1073 }
1074
1075 #[test]
1078 fn attr_appends_arbitrary_attributes() {
1079 let rendered = input_named("text", "email")
1080 .attr("placeholder", "Enter email")
1081 .attr("required", "true")
1082 .render();
1083
1084 assert_eq!(
1085 rendered,
1086 r#"<input type="text" name="email" placeholder="Enter email" required="true">"#
1087 );
1088 }
1089}