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
13pub struct Element {
32 tag: Cow<'static, str>,
35 attributes: Vec<Attribute>,
36 content: Option<String>,
37 children: Vec<Node>,
38 self_closing: bool,
39}
40
41const DEBUG_MAX_DEPTH: usize = 128;
50
51pub(crate) struct DebugElement<'a>(pub(crate) &'a Element, pub(crate) usize);
53
54impl std::fmt::Debug for DebugElement<'_> {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 let Self(element, depth) = *self;
57 f.debug_struct("Element")
58 .field("tag", &element.tag)
59 .field("attributes", &element.attributes)
60 .field("content", &element.content)
61 .field("children", &DebugChildren(&element.children, depth + 1))
62 .field("self_closing", &element.self_closing)
63 .finish()
64 }
65}
66
67pub(crate) struct DebugChildren<'a>(pub(crate) &'a [Node], pub(crate) usize);
70
71impl std::fmt::Debug for DebugChildren<'_> {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 let Self(children, depth) = *self;
74 if depth > DEBUG_MAX_DEPTH {
75 return f.write_str("...");
76 }
77 f.debug_list()
78 .entries(children.iter().map(|child| DebugNode(child, depth)))
79 .finish()
80 }
81}
82
83pub(crate) struct DebugNode<'a>(pub(crate) &'a Node, pub(crate) usize);
85
86impl std::fmt::Debug for DebugNode<'_> {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 let Self(node, depth) = *self;
89 match node {
90 Node::Element(element) => f
91 .debug_tuple("Element")
92 .field(&DebugElement(element, depth))
93 .finish(),
94 Node::Text(text) => f.debug_tuple("Text").field(text).finish(),
95 Node::Raw(text) => f.debug_tuple("Raw").field(text).finish(),
96 Node::Comment(text) => f.debug_tuple("Comment").field(text).finish(),
97 Node::Fragment(children) => f
98 .debug_tuple("Fragment")
99 .field(&DebugChildren(children, depth + 1))
100 .finish(),
101 }
102 }
103}
104
105impl std::fmt::Debug for Element {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 DebugElement(self, 0).fmt(f)
125 }
126}
127
128impl Clone for Element {
129 fn clone(&self) -> Self {
130 match clone_tree(Source::Element(self)) {
131 Node::Element(element) => element,
132 _ => unreachable!("cloning an element yields an element"),
133 }
134 }
135}
136
137impl PartialEq for Element {
138 fn eq(&self, other: &Self) -> bool {
139 trees_equal(Source::Element(self), Source::Element(other))
140 }
141}
142
143impl Eq for Element {}
144
145impl std::fmt::Display for Element {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.write_str(&if f.alternate() {
149 self.render_pretty()
150 } else {
151 self.render()
152 })
153 }
154}
155
156impl std::hash::Hash for Element {
157 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
158 hash_tree(Source::Element(self), state);
159 }
160}
161
162impl<N: Into<Node>> Extend<N> for Element {
164 fn extend<I: IntoIterator<Item = N>>(&mut self, iter: I) {
165 self.children.extend(iter.into_iter().map(Into::into));
166 }
167}
168
169pub(crate) struct Descendants<'a> {
172 pending: Vec<&'a Node>,
173}
174
175impl<'a> Descendants<'a> {
176 pub(crate) fn new(children: &'a [Node]) -> Self {
177 Self {
178 pending: children.iter().rev().collect(),
179 }
180 }
181}
182
183impl<'a> Iterator for Descendants<'a> {
184 type Item = &'a Element;
185
186 fn next(&mut self) -> Option<&'a Element> {
187 while let Some(node) = self.pending.pop() {
188 match node {
189 Node::Element(element) => {
190 self.pending.extend(element.children.iter().rev());
191 return Some(element);
192 }
193 Node::Fragment(children) => self.pending.extend(children.iter().rev()),
194 _ => {}
195 }
196 }
197 None
198 }
199}
200
201pub(crate) fn hash_tree<H: std::hash::Hasher>(root: Source<'_>, state: &mut H) {
204 use std::hash::Hash;
205
206 let mut pending = vec![root];
207 while let Some(source) = pending.pop() {
208 match source {
209 Source::Element(element) | Source::Node(Node::Element(element)) => {
210 0_u8.hash(state);
211 element.tag.hash(state);
212 element.attributes.hash(state);
213 element.content.hash(state);
214 element.self_closing.hash(state);
215 element.children.len().hash(state);
216 pending.extend(element.children.iter().rev().map(Source::Node));
217 }
218 Source::Node(Node::Fragment(children)) => {
219 1_u8.hash(state);
220 children.len().hash(state);
221 pending.extend(children.iter().rev().map(Source::Node));
222 }
223 Source::Node(Node::Text(text)) => {
224 2_u8.hash(state);
225 text.hash(state);
226 }
227 Source::Node(Node::Raw(text)) => {
228 3_u8.hash(state);
229 text.hash(state);
230 }
231 Source::Node(Node::Comment(text)) => {
232 4_u8.hash(state);
233 text.hash(state);
234 }
235 }
236 }
237}
238
239fn shell(element: &Element) -> Element {
241 Element {
242 tag: element.tag.clone(),
243 attributes: element.attributes.clone(),
244 content: element.content.clone(),
245 children: Vec::with_capacity(element.children.len()),
246 self_closing: element.self_closing,
247 }
248}
249
250enum CloneFrame<'a> {
252 Element {
253 source: &'a [Node],
254 built: Element,
255 },
256 Fragment {
257 source: &'a [Node],
258 built: Vec<Node>,
259 },
260}
261
262pub(crate) fn clone_tree(root: Source<'_>) -> Node {
264 let mut stack: Vec<CloneFrame<'_>> = Vec::new();
265 let mut descend = Some(root);
266
267 loop {
268 let mut finished = None;
270 if let Some(source) = descend.take() {
271 match source {
272 Source::Element(element) | Source::Node(Node::Element(element)) => {
273 stack.push(CloneFrame::Element {
274 source: &element.children,
275 built: shell(element),
276 });
277 }
278 Source::Node(Node::Fragment(children)) => stack.push(CloneFrame::Fragment {
279 source: children,
280 built: Vec::with_capacity(children.len()),
281 }),
282 Source::Node(Node::Text(text)) => finished = Some(Node::Text(text.clone())),
283 Source::Node(Node::Raw(text)) => finished = Some(Node::Raw(text.clone())),
284 Source::Node(Node::Comment(text)) => finished = Some(Node::Comment(text.clone())),
285 }
286 }
287
288 if finished.is_none() {
289 let Some(frame) = stack.last_mut() else {
291 return Node::Fragment(Vec::new());
293 };
294 let (source, built_len) = match frame {
295 CloneFrame::Element { source, built } => (*source, built.children.len()),
296 CloneFrame::Fragment { source, built } => (*source, built.len()),
297 };
298 if let Some(next) = source.get(built_len) {
299 descend = Some(Source::Node(next));
300 continue;
301 }
302 finished = stack.pop().map(|frame| match frame {
303 CloneFrame::Element { built, .. } => Node::Element(built),
304 CloneFrame::Fragment { built, .. } => Node::Fragment(built),
305 });
306 }
307
308 let Some(done) = finished else { continue };
309 match stack.last_mut() {
310 None => return done,
311 Some(CloneFrame::Element { built, .. }) => built.children.push(done),
312 Some(CloneFrame::Fragment { built, .. }) => built.push(done),
313 }
314 }
315}
316
317pub(crate) fn trees_equal(left: Source<'_>, right: Source<'_>) -> bool {
319 let mut pending = vec![(left, right)];
320 while let Some((left, right)) = pending.pop() {
321 match (left, right) {
322 (
323 Source::Element(a) | Source::Node(Node::Element(a)),
324 Source::Element(b) | Source::Node(Node::Element(b)),
325 ) => {
326 if a.tag != b.tag
327 || a.attributes != b.attributes
328 || a.content != b.content
329 || a.self_closing != b.self_closing
330 || a.children.len() != b.children.len()
331 {
332 return false;
333 }
334 pending.extend(
335 a.children
336 .iter()
337 .zip(&b.children)
338 .map(|(a, b)| (Source::Node(a), Source::Node(b))),
339 );
340 }
341 (Source::Node(Node::Fragment(a)), Source::Node(Node::Fragment(b))) => {
342 if a.len() != b.len() {
343 return false;
344 }
345 pending.extend(
346 a.iter()
347 .zip(b)
348 .map(|(a, b)| (Source::Node(a), Source::Node(b))),
349 );
350 }
351 (Source::Node(Node::Text(a)), Source::Node(Node::Text(b)))
352 | (Source::Node(Node::Raw(a)), Source::Node(Node::Raw(b)))
353 | (Source::Node(Node::Comment(a)), Source::Node(Node::Comment(b))) => {
354 if a != b {
355 return false;
356 }
357 }
358 _ => return false,
359 }
360 }
361 true
362}
363
364impl Element {
365 pub fn new(tag: impl Into<String>) -> Self {
367 Self::with_tag(Cow::Owned(tag.into()))
368 }
369
370 #[must_use]
381 pub fn from_static(tag: &'static str) -> Self {
382 Self::with_tag(Cow::Borrowed(tag))
383 }
384
385 fn with_tag(tag: Cow<'static, str>) -> Self {
386 Self {
387 tag,
388 attributes: Vec::new(),
389 content: None,
390 children: Vec::new(),
391 self_closing: false,
392 }
393 }
394
395 #[must_use]
399 pub fn tag(&self) -> &str {
400 &self.tag
401 }
402
403 #[must_use]
405 pub fn attributes(&self) -> &[Attribute] {
406 &self.attributes
407 }
408
409 #[must_use]
411 pub fn content(&self) -> Option<&str> {
412 self.content.as_deref()
413 }
414
415 #[must_use]
417 pub fn children(&self) -> &[Node] {
418 &self.children
419 }
420
421 #[must_use]
433 pub fn is_self_closing(&self) -> bool {
434 self.self_closing
435 }
436
437 #[must_use]
460 pub fn find(&self, tag: &str) -> Option<&Element> {
461 Descendants::new(&self.children).find(|element| element.tag() == tag)
462 }
463
464 pub fn find_all<'a>(&'a self, tag: &'a str) -> impl Iterator<Item = &'a Element> + 'a {
476 Descendants::new(&self.children).filter(move |element| element.tag() == tag)
477 }
478
479 #[must_use]
497 pub fn attribute(&self, name: &str) -> Option<&str> {
498 self.attributes
499 .iter()
500 .find(|attribute| attribute.key() == name)
501 .map(Attribute::value)
502 }
503
504 #[must_use]
524 pub fn text(mut self, content: impl AsRef<str>) -> Self {
525 let raw = content.as_ref();
526 if self.children.is_empty() {
527 let slot = self
528 .content
529 .get_or_insert_with(|| String::with_capacity(raw.len()));
530 write_escaped_text(slot, raw, false);
531 } else {
532 self.children.push(Node::text(raw));
533 }
534 self
535 }
536
537 #[must_use]
556 pub fn raw_text(mut self, content: impl Into<String>) -> Self {
557 let content = content.into();
558 if !self.children.is_empty() {
559 self.children.push(Node::raw(content));
560 } else if let Some(slot) = &mut self.content {
561 slot.push_str(&content);
562 } else {
563 self.content = Some(content);
564 }
565 self
566 }
567
568 #[must_use]
570 pub fn child(mut self, child: impl Into<Node>) -> Self {
571 self.children.push(child.into());
572 self
573 }
574
575 #[must_use]
577 pub fn children_from<N: Into<Node>>(mut self, children: impl IntoIterator<Item = N>) -> Self {
578 self.children.extend(children.into_iter().map(Into::into));
579 self
580 }
581
582 #[must_use]
613 pub fn csp_hash(&self) -> Option<String> {
614 if !matches!(self.tag(), "script" | "style") || self.self_closing {
615 return None;
616 }
617 let mut body = String::new();
618 if let Some(content) = &self.content {
619 body.push_str(content);
620 }
621 let options = RenderOptions::compact();
622 for child in &self.children {
623 child.write_into(&mut body, &options, 0);
624 }
625 let digest = crate::core::digest::sha256(body.as_bytes());
626 Some(format!("sha256-{}", crate::core::digest::base64(&digest)))
627 }
628
629 #[must_use]
633 pub fn add_attribute(mut self, attribute: Attribute) -> Self {
634 self.attributes.push(attribute);
635 self
636 }
637
638 #[must_use]
641 pub fn attr(self, key: impl Into<String>, value: impl AsRef<str>) -> Self {
642 self.add_attribute(Attribute::new(key, value))
643 }
644
645 #[must_use]
647 pub fn bool_attr(self, key: impl Into<String>) -> Self {
648 self.add_attribute(Attribute::boolean(key))
649 }
650
651 #[must_use]
666 pub fn trusted_url(mut self, key: impl Into<String>, value: impl AsRef<str>) -> Self {
667 let attribute = Attribute::trusted_url(key, value);
668 self.attributes
671 .retain(|existing| existing.key() != attribute.key());
672 self.attributes.push(attribute);
673 self
674 }
675
676 #[must_use]
695 pub fn self_closing(mut self, yes: bool) -> Self {
696 self.self_closing = yes;
697 self
698 }
699
700 #[must_use]
702 pub fn data_attr(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
703 self.attr(format!("data-{}", key.as_ref()), value)
704 }
705
706 #[must_use]
712 pub fn data_attrs<K: AsRef<str>, V: AsRef<str>>(
713 mut self,
714 data: impl IntoIterator<Item = (K, V)>,
715 ) -> Self {
716 for (key, value) in data {
717 self = self.data_attr(key, value);
718 }
719 self
720 }
721
722 #[must_use]
724 pub fn aria_attr(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
725 self.attr(format!("aria-{}", key.as_ref()), value)
726 }
727
728 #[must_use]
730 pub fn aria_attrs<K: AsRef<str>, V: AsRef<str>>(
731 mut self,
732 aria: impl IntoIterator<Item = (K, V)>,
733 ) -> Self {
734 for (key, value) in aria {
735 self = self.aria_attr(key, value);
736 }
737 self
738 }
739
740 #[must_use]
747 pub fn set_id(self, id: impl AsRef<str>) -> Self {
748 self.replace_attribute("id", id.as_ref())
749 }
750
751 #[must_use]
753 pub fn set_style(self, style: impl AsRef<str>) -> Self {
754 self.replace_attribute("style", style.as_ref())
755 }
756
757 #[must_use]
759 pub fn set_role(self, role: impl AsRef<str>) -> Self {
760 self.replace_attribute("role", role.as_ref())
761 }
762
763 fn replace_attribute(mut self, key: &'static str, value: &str) -> Self {
764 self.attributes.retain(|a| a.key() != key);
765 self.attributes.push(Attribute::new_static(key, value));
766 self
767 }
768
769 #[must_use]
776 pub fn add_class(mut self, class_name: impl AsRef<str>) -> Self {
777 let raw = class_name.as_ref();
778 if let Some(existing) = self.attributes.iter_mut().find(|a| a.key() == "class") {
779 existing.push_escaped_word(raw);
780 } else {
781 self.attributes.push(Attribute::new_static("class", raw));
782 }
783 self
784 }
785
786 #[must_use]
788 pub fn add_classes<S: AsRef<str>>(mut self, class_names: impl IntoIterator<Item = S>) -> Self {
789 for name in class_names {
790 self = self.add_class(name);
791 }
792 self
793 }
794}
795
796impl Render for Element {
797 fn write_into(&self, out: &mut String, options: &RenderOptions, depth: usize) {
798 crate::core::node::write_element_tree(self, out, options, depth);
799 }
800}
801
802impl Drop for Element {
811 fn drop(&mut self) {
812 let mut pending = core::mem::take(&mut self.children);
813 while let Some(node) = pending.pop() {
814 match node {
815 Node::Element(mut element) => pending.append(&mut element.children),
816 Node::Fragment(mut children) => pending.append(&mut children),
817 Node::Text(_) | Node::Raw(_) | Node::Comment(_) => {}
818 }
819 }
820 }
821}
822
823#[cfg(test)]
824mod tests {
825
826 #[test]
828 fn a_clone_equals_its_source_and_is_independent() {
829 let source = div()
830 .attr("id", "a")
831 .add_class("x y")
832 .child(p().text("t").child(Node::comment("c")))
833 .child(Node::fragment([
834 Node::text("a"),
835 Node::raw("<b>"),
836 Node::fragment([]),
837 span().into(),
838 ]))
839 .child(Node::text("tail"));
840 let copy = source.clone();
841 assert_eq!(source, copy);
842 assert_eq!(source.render(), copy.render());
843
844 let changed = copy.clone().child(Node::text("more"));
845 assert_ne!(source, changed);
846 assert_eq!(source.render(), copy.render(), "the original is untouched");
847 }
848
849 #[test]
850 fn equality_sees_every_field() {
851 let base = div().attr("id", "a").text("t");
852 assert_eq!(base, div().attr("id", "a").text("t"));
853 assert_ne!(base, div().attr("id", "b").text("t"));
854 assert_ne!(base, div().attr("id", "a").text("u"));
855 assert_ne!(base, span().attr("id", "a").text("t"));
856 assert_ne!(base, div().attr("id", "a").text("t").self_closing(true));
857 assert_ne!(Node::text("a"), Node::raw("a"));
858 assert_ne!(Node::text("a"), Node::comment("a"));
859 assert_ne!(Node::fragment([]), Node::fragment([Node::text("")]));
860 assert_eq!(Node::from(base.clone()), Node::from(base));
861 }
862
863 use super::*;
864
865 #[test]
868 fn debug_writes_what_the_derive_wrote() {
869 let tree = div()
870 .attr("id", "x")
871 .bool_attr("hidden")
872 .text("lead")
873 .child(p().text("a<b"))
874 .child(Node::fragment([
875 Node::raw("<i>"),
876 Node::comment("c"),
877 Node::text("t"),
878 ]));
879 assert_eq!(
880 format!("{tree:?}"),
881 r#"Element { tag: "div", attributes: [Attribute { key: "id", value: "x", is_boolean: false, is_trusted_url: false }, Attribute { key: "hidden", value: "", is_boolean: true, is_trusted_url: false }], content: Some("lead"), children: [Element(Element { tag: "p", attributes: [], content: Some("a<b"), children: [], self_closing: false }), Fragment([Raw(Markup("<i>")), Comment(Markup("c")), Text(Markup("t"))])], self_closing: false }"#
882 );
883 assert_eq!(
884 format!("{:#?}", p().text("x").attr("id", "y")),
885 "Element {\n tag: \"p\",\n attributes: [\n Attribute {\n key: \"id\",\n value: \"y\",\n is_boolean: false,\n is_trusted_url: false,\n },\n ],\n content: Some(\n \"x\",\n ),\n children: [],\n self_closing: false,\n}"
886 );
887 }
888
889 fn chain(levels: usize) -> Element {
891 let mut tree = crate::elements::div();
892 for _ in 1..levels {
893 tree = crate::elements::div().child(tree);
894 }
895 tree
896 }
897
898 #[test]
899 fn debug_shows_the_whole_tree_up_to_the_depth_limit_and_dots_below_it() {
900 let at_the_limit = format!("{:?}", chain(DEBUG_MAX_DEPTH));
901 assert!(!at_the_limit.contains("..."), "{at_the_limit}");
902
903 let one_past = format!("{:?}", chain(DEBUG_MAX_DEPTH + 1));
904 assert_eq!(one_past.matches("...").count(), 1, "{one_past}");
905 assert!(one_past.contains("children: ..."), "{one_past}");
906 }
907
908 #[test]
909 fn debug_of_a_fragment_chain_is_bounded_too() {
910 let mut node = Node::fragment([]);
911 for _ in 0..100_000 {
912 node = Node::fragment([node]);
913 }
914 let node = Node::from(crate::elements::div().child(node));
917 let shown = format!("{node:?}");
918 assert!(shown.contains("..."));
919 assert!(shown.len() < 100_000, "{} bytes", shown.len());
920 }
921
922 #[test]
923 fn csp_hash_covers_the_content_as_it_is_written_into_the_page() {
924 assert_eq!(
926 crate::elements::style()
927 .raw_text("body{color:red}")
928 .csp_hash()
929 .as_deref(),
930 Some("sha256-FcQqt3aNlV7AZnGV4zkQRVeCeJOxbMPnQSx258L803E=")
931 );
932 assert_eq!(
934 script().text("a<b").csp_hash().as_deref(),
935 Some("sha256-ElKEL8tlu/71khLOyHevXk0zDglt1/FzVBvpN3U1gyU=")
936 );
937 }
938
939 #[test]
940 fn csp_hash_is_the_hash_of_what_sits_between_the_tags() {
941 let element = script()
942 .raw_text("let a = 1;")
943 .child(Node::raw(" let b = 2;"));
944 let rendered = element.render();
945 let inner = rendered
946 .strip_prefix("<script>")
947 .and_then(|rest| rest.strip_suffix("</script>"))
948 .expect("one script");
949 assert_eq!(inner, "let a = 1; let b = 2;");
950 let digest = crate::core::digest::sha256(inner.as_bytes());
951 assert_eq!(
952 element.csp_hash(),
953 Some(format!("sha256-{}", crate::core::digest::base64(&digest)))
954 );
955 }
956
957 #[test]
958 fn csp_hash_is_none_outside_script_and_style_and_tracks_every_byte() {
959 assert_eq!(div().text("x").csp_hash(), None);
960 assert_eq!(script().self_closing(true).csp_hash(), None);
961 assert_ne!(
962 crate::elements::style().raw_text("a{}").csp_hash(),
963 crate::elements::style().raw_text("a{} ").csp_hash()
964 );
965 assert!(
966 script().csp_hash().is_some(),
967 "an empty script still has a hash"
968 );
969 }
970 use crate::core::RenderOptions;
971 use crate::elements::{
972 a, body, button, div, footer, head, header, html_tag, img, input_named, li, main_tag, meta,
973 nav, ol, p, script, span, stylesheet, table, td, th, title, tr, ul,
974 };
975
976 #[test]
978 fn trusted_url_replaces_a_blocked_value() {
979 assert_eq!(
980 a().attr("href", "javascript:x")
981 .trusted_url("href", "javascript:print()")
982 .render(),
983 r#"<a href="javascript:print()"></a>"#
984 );
985 }
986
987 #[test]
988 fn a_non_void_tag_marked_self_closing_drops_its_children() {
989 let tag = div()
990 .attr("id", "x")
991 .self_closing(true)
992 .child(span().text("no"));
993 assert_eq!(tag.render(), r#"<div id="x">"#);
994 assert_eq!(
995 tag.render_with(&RenderOptions::compact().with_xhtml_self_closing(true)),
996 r#"<div id="x" />"#
997 );
998 assert_eq!(tag.render_pretty(), r#"<div id="x">"#);
999 assert!(!div().is_self_closing());
1000 }
1001
1002 #[test]
1003 fn add_class_appends_to_one_attribute() {
1004 assert_eq!(
1005 div().add_class("a").add_class("b").render(),
1006 r#"<div class="a b"></div>"#
1007 );
1008 }
1009
1010 #[test]
1011 fn chaining_add_class_does_not_double_escape() {
1012 let rendered = div().add_class("a&b").add_class("c").render();
1013 assert_eq!(rendered, r#"<div class="a&b c"></div>"#);
1014 assert!(!rendered.contains("&amp;"));
1015 }
1016
1017 #[test]
1018 fn a_quote_in_a_class_name_cannot_break_out() {
1019 let rendered = div().add_class(r#"a" onload="alert(1)"#).render();
1020 assert!(!rendered.contains("onload=\""));
1021 assert!(rendered.contains("""));
1022 }
1023
1024 #[test]
1025 fn add_classes_appends_all_of_them_in_order() {
1026 assert_eq!(
1027 div().add_classes(["card", "p-4", "shadow"]).render(),
1028 r#"<div class="card p-4 shadow"></div>"#
1029 );
1030 }
1031
1032 #[test]
1033 fn set_id_replaces_rather_than_appending() {
1034 let rendered = div().set_id("first").set_id("second").render();
1035 assert_eq!(rendered, r#"<div id="second"></div>"#);
1036 }
1037
1038 #[test]
1039 fn set_style_and_set_role_also_replace() {
1040 let rendered = div()
1041 .set_style("color:red")
1042 .set_style("color:blue")
1043 .set_role("main")
1044 .render();
1045 assert_eq!(rendered, r#"<div style="color:blue" role="main"></div>"#);
1046 }
1047
1048 #[test]
1049 fn data_and_aria_attributes_get_their_prefixes() {
1050 let rendered = span()
1051 .data_attr("id", "7")
1052 .aria_attr("label", "Close")
1053 .render();
1054 assert_eq!(rendered, r#"<span data-id="7" aria-label="Close"></span>"#);
1055 }
1056
1057 #[test]
1059 fn bulk_attribute_order_is_stable() {
1060 let build = || {
1061 div()
1062 .data_attrs([("a", "1"), ("b", "2"), ("c", "3")])
1063 .render()
1064 };
1065 let expected = r#"<div data-a="1" data-b="2" data-c="3"></div>"#;
1066 for _ in 0..16 {
1067 assert_eq!(build(), expected);
1068 }
1069 }
1070
1071 #[test]
1074 fn plain_attributes_are_appended_without_deduplication() {
1075 assert_eq!(
1076 div().attr("data-x", "1").attr("data-x", "2").render(),
1077 r#"<div data-x="1" data-x="2"></div>"#
1078 );
1079 }
1080
1081 #[test]
1084 fn text_is_escaped_and_raw_text_is_not() {
1085 assert_eq!(p().text("<b>").render(), "<p><b></p>");
1086 assert_eq!(p().raw_text("<b>").render(), "<p><b></p>");
1087 }
1088
1089 #[test]
1090 fn boolean_attributes_render_bare() {
1091 assert_eq!(
1092 button().bool_attr("disabled").render(),
1093 "<button disabled></button>"
1094 );
1095 }
1096
1097 #[test]
1098 fn children_from_appends_a_sequence() {
1099 let list = div().children_from([p().text("a"), p().text("b")]);
1100 assert_eq!(list.render(), "<div><p>a</p><p>b</p></div>");
1101 }
1102
1103 #[test]
1104 fn a_tag_renders_its_attributes_then_its_content() {
1105 let tag = Element::new("p")
1106 .attr("class", "text")
1107 .text("Hello, World!");
1108
1109 assert_eq!(tag.render(), r#"<p class="text">Hello, World!</p>"#);
1110 }
1111
1112 #[test]
1115 fn extra_attributes_can_precede_the_typed_ones() {
1116 let tag = img()
1117 .attr("width", "100")
1118 .attr("height", "100")
1119 .attr("src", "image.png")
1120 .attr("alt", "An image");
1121
1122 assert_eq!(
1123 tag.render(),
1124 r#"<img width="100" height="100" src="image.png" alt="An image">"#
1125 );
1126 }
1127
1128 #[test]
1129 fn nested_children_render_in_order() {
1130 let document = html_tag().child(
1131 div()
1132 .child(p().text("This is a paragraph."))
1133 .child(img().attr("src", "image.png").attr("alt", "An image")),
1134 );
1135
1136 assert_eq!(
1137 document.render(),
1138 concat!(
1139 "<html><div><p>This is a paragraph.</p>",
1140 r#"<img src="image.png" alt="An image"></div></html>"#,
1141 )
1142 );
1143 }
1144
1145 #[test]
1146 fn a_container_renders_its_class_before_its_children() {
1147 let document = html_tag().child(
1148 div()
1149 .add_class("main-body")
1150 .child(p().text("Title"))
1151 .child(p().text("This is a paragraph.")),
1152 );
1153
1154 assert_eq!(
1155 document.render(),
1156 r#"<html><div class="main-body"><p>Title</p><p>This is a paragraph.</p></div></html>"#
1157 );
1158 }
1159
1160 #[test]
1161 fn a_table_renders_its_rows_and_cells() {
1162 let document = html_tag().child(
1163 table()
1164 .add_class("table")
1165 .child(
1166 tr().child(th().text("Header 1"))
1167 .child(th().text("Header 2")),
1168 )
1169 .child(
1170 tr().child(td().text("Row 1, Cell 1"))
1171 .child(td().text("Row 1, Cell 2")),
1172 )
1173 .child(
1174 tr().child(td().text("Row 2, Cell 1"))
1175 .child(td().text("Row 2, Cell 2")),
1176 ),
1177 );
1178
1179 assert_eq!(
1180 document.render(),
1181 concat!(
1182 r#"<html><table class="table"><tr><th>Header 1</th><th>Header 2</th></tr>"#,
1183 "<tr><td>Row 1, Cell 1</td><td>Row 1, Cell 2</td></tr>",
1184 "<tr><td>Row 2, Cell 1</td><td>Row 2, Cell 2</td></tr></table></html>",
1185 )
1186 );
1187 }
1188
1189 #[test]
1190 fn an_unordered_list_renders_its_items() {
1191 let document = html_tag().child(
1192 ul().add_class("unordered-list")
1193 .child(li().text("Item 1"))
1194 .child(li().text("Item 2"))
1195 .child(li().text("Item 3")),
1196 );
1197
1198 assert_eq!(
1199 document.render(),
1200 concat!(
1201 r#"<html><ul class="unordered-list">"#,
1202 "<li>Item 1</li><li>Item 2</li><li>Item 3</li></ul></html>",
1203 )
1204 );
1205 }
1206
1207 #[test]
1208 fn an_ordered_list_renders_its_items() {
1209 let document = html_tag().child(
1210 ol().add_class("ordered-list")
1211 .child(li().text("First"))
1212 .child(li().text("Second"))
1213 .child(li().text("Third")),
1214 );
1215
1216 assert_eq!(
1217 document.render(),
1218 concat!(
1219 r#"<html><ol class="ordered-list">"#,
1220 "<li>First</li><li>Second</li><li>Third</li></ol></html>",
1221 )
1222 );
1223 }
1224
1225 #[test]
1228 fn a_description_list_renders_terms_and_descriptions() {
1229 let document = html_tag().child(
1230 Element::new("dl")
1231 .add_class("description-list")
1232 .child(Element::new("dt").text("Term 1"))
1233 .child(Element::new("dd").text("Description 1"))
1234 .child(Element::new("dt").text("Term 2"))
1235 .child(Element::new("dd").text("Description 2")),
1236 );
1237
1238 assert_eq!(
1239 document.render(),
1240 concat!(
1241 r#"<html><dl class="description-list">"#,
1242 "<dt>Term 1</dt><dd>Description 1</dd>",
1243 "<dt>Term 2</dt><dd>Description 2</dd></dl></html>",
1244 )
1245 );
1246 }
1247
1248 #[test]
1249 fn the_structural_tags_nest_into_a_page() {
1250 let document = html_tag()
1251 .child(
1252 head()
1253 .child(
1254 meta()
1255 .attr("name", "description")
1256 .attr("content", "A description of the page"),
1257 )
1258 .child(stylesheet("styles.css")),
1259 )
1260 .child(
1261 body()
1262 .child(
1263 header().child(
1264 nav()
1265 .child(a().attr("href", "#home").text("Home"))
1266 .child(a().attr("href", "#about").text("About"))
1267 .child(a().attr("href", "#contact").text("Contact")),
1268 ),
1269 )
1270 .child(main_tag().child(p().text("Welcome to our website!")))
1271 .child(footer().child(p().text("\u{a9} 2024 Company, Inc."))),
1272 );
1273
1274 assert_eq!(
1275 document.render(),
1276 concat!(
1277 r#"<html><head><meta name="description" content="A description of the page">"#,
1278 r#"<link href="styles.css" rel="stylesheet"></head><body><header><nav>"#,
1279 r##"<a href="#home">Home</a><a href="#about">About</a>"##,
1280 r##"<a href="#contact">Contact</a></nav></header>"##,
1281 "<main><p>Welcome to our website!</p></main>",
1282 "<footer><p>\u{a9} 2024 Company, Inc.</p></footer></body></html>",
1283 )
1284 );
1285 }
1286
1287 #[test]
1290 fn a_script_body_is_not_escaped() {
1291 let document = html_tag().child(
1292 script()
1293 .attr("type", "text/javascript")
1294 .raw_text("alert('Hello World');"),
1295 );
1296
1297 assert_eq!(
1298 document.render(),
1299 r#"<html><script type="text/javascript">alert('Hello World');</script></html>"#
1300 );
1301 }
1302
1303 #[test]
1304 fn meta_renders_both_the_named_and_the_charset_form() {
1305 let document = html_tag()
1306 .child(
1307 meta()
1308 .attr("name", "description")
1309 .attr("content", "A description of the page"),
1310 )
1311 .child(meta().attr("charset", "utf-8"));
1312
1313 assert_eq!(
1314 document.render(),
1315 concat!(
1316 r#"<html><meta name="description" content="A description of the page">"#,
1317 r#"<meta charset="utf-8"></html>"#,
1318 )
1319 );
1320 }
1321
1322 #[test]
1323 fn a_title_renders_its_text() {
1324 let document = html_tag().child(title().text("Title my site"));
1325
1326 assert_eq!(
1327 document.render(),
1328 "<html><title>Title my site</title></html>"
1329 );
1330 }
1331
1332 #[test]
1333 fn a_span_renders_its_attributes_and_content() {
1334 let tag = span().attr("class", "text").text("Hello, World!");
1335
1336 assert_eq!(tag.render(), r#"<span class="text">Hello, World!</span>"#);
1337 }
1338
1339 #[test]
1342 fn a_button_carries_its_type_after_its_class() {
1343 let document = html_tag().child(button().add_class("button-class").attr("type", "button"));
1344
1345 assert_eq!(
1346 document.render(),
1347 r#"<html><button class="button-class" type="button"></button></html>"#
1348 );
1349 }
1350
1351 #[test]
1352 fn a_button_renders_its_children() {
1353 let document = html_tag().child(
1354 button()
1355 .add_class("button-class")
1356 .attr("type", "button")
1357 .child(span().add_class("icon-bar")),
1358 );
1359
1360 assert_eq!(
1361 document.render(),
1362 concat!(
1363 r#"<html><button class="button-class" type="button">"#,
1364 r#"<span class="icon-bar"></span></button></html>"#,
1365 )
1366 );
1367 }
1368
1369 #[test]
1372 fn chaining_helpers_keeps_the_element_usable() {
1373 let card: Element = div().add_class("card").set_id("hero").set_role("region");
1374
1375 assert_eq!(
1376 card.render(),
1377 r#"<div class="card" id="hero" role="region"></div>"#
1378 );
1379 }
1380
1381 #[test]
1382 fn set_style_escapes_quotes() {
1383 let rendered = div()
1384 .set_style(r#"font-family: "Inter", sans-serif"#)
1385 .render();
1386
1387 assert_eq!(
1388 rendered,
1389 r#"<div style="font-family: "Inter", sans-serif"></div>"#
1390 );
1391 }
1392
1393 #[test]
1394 fn a_single_class_renders_on_its_own() {
1395 assert_eq!(
1396 div().add_class("container").render(),
1397 r#"<div class="container"></div>"#
1398 );
1399 }
1400
1401 #[test]
1402 fn set_id_renders_an_id_attribute() {
1403 assert_eq!(
1404 div().set_id("main-content").render(),
1405 r#"<div id="main-content"></div>"#
1406 );
1407 }
1408
1409 #[test]
1410 fn the_helpers_chain_in_the_order_they_are_called() {
1411 let rendered = div()
1412 .set_id("content")
1413 .add_class("container")
1414 .add_class("active")
1415 .set_style("padding: 20px;")
1416 .render();
1417
1418 assert_eq!(
1419 rendered,
1420 r#"<div id="content" class="container active" style="padding: 20px;"></div>"#
1421 );
1422 }
1423
1424 #[test]
1427 fn several_data_attributes_keep_the_order_they_are_given() {
1428 let rendered = div()
1429 .data_attrs([("id", "123"), ("type", "product")])
1430 .render();
1431
1432 assert_eq!(rendered, r#"<div data-id="123" data-type="product"></div>"#);
1433 }
1434
1435 #[test]
1436 fn several_aria_attributes_keep_the_order_they_are_given() {
1437 let rendered = nav()
1438 .aria_attrs([("label", "Main navigation"), ("expanded", "true")])
1439 .render();
1440
1441 assert_eq!(
1442 rendered,
1443 r#"<nav aria-label="Main navigation" aria-expanded="true"></nav>"#
1444 );
1445 }
1446
1447 #[test]
1450 fn attr_appends_arbitrary_attributes() {
1451 let rendered = input_named("text", "email")
1452 .attr("placeholder", "Enter email")
1453 .attr("required", "true")
1454 .render();
1455
1456 assert_eq!(
1457 rendered,
1458 r#"<input type="text" name="email" placeholder="Enter email" required="true">"#
1459 );
1460 }
1461}