1use crate::vdom::Style;
5use crate::vdom::Value;
6use crate::{
7 vdom::GroupedAttributeValues,
8 vdom::{Attribute, Element, Leaf, Node},
9};
10use std::fmt;
11
12const DEFAULT_INDENT_SIZE: usize = 2;
13
14fn maybe_indent(buffer: &mut dyn fmt::Write, indent: usize, compressed: bool) -> fmt::Result {
16 if !compressed {
17 write!(
18 buffer,
19 "\n{}",
20 " ".repeat(DEFAULT_INDENT_SIZE).repeat(indent)
21 )?;
22 }
23 Ok(())
24}
25
26impl<MSG> Node<MSG> {
27 pub fn render_with_indent(
46 &self,
47 buffer: &mut dyn fmt::Write,
48 indent: usize,
49 compressed: bool,
50 ) -> fmt::Result {
51 match self {
52 Node::Element(element) => element.render_with_indent(buffer, indent, compressed),
53 Node::Leaf(leaf) => leaf.render_with_indent(buffer, indent, compressed),
54 }
55 }
56
57 pub fn render(&self, buffer: &mut dyn fmt::Write) -> fmt::Result {
59 self.render_with_indent(buffer, 0, false)
60 }
61
62 fn render_compressed(&self, buffer: &mut dyn fmt::Write) -> fmt::Result {
64 self.render_with_indent(buffer, 0, true)
65 }
66
67 pub fn render_to_string(&self) -> String {
69 let mut buffer = String::new();
70 self.render_compressed(&mut buffer).expect("must render");
71 buffer
72 }
73
74 pub fn render_to_string_pretty(&self) -> String {
76 let mut buffer = String::new();
77 self.render(&mut buffer).expect("must render");
78 buffer
79 }
80}
81
82impl<MSG> Leaf<MSG> {
83 pub fn render_with_indent(
85 &self,
86 buffer: &mut dyn fmt::Write,
87 indent: usize,
88 compressed: bool,
89 ) -> fmt::Result {
90 match self {
91 Leaf::Text(text) => {
92 write!(buffer, "{text}")
93 }
94 Leaf::Symbol(symbol) => {
95 write!(buffer, "{symbol}")
96 }
97 Leaf::Comment(comment) => {
98 write!(buffer, "<!--{comment}-->")
99 }
100 Leaf::DocType(doctype) => {
101 write!(buffer, "<!doctype {doctype}>")
102 }
103 Leaf::Fragment(nodes) => {
104 for node in nodes {
105 node.render_with_indent(buffer, indent, compressed)?;
106 }
107 Ok(())
108 }
109 Leaf::NodeList(node_list) => {
110 for node in node_list {
111 node.render_with_indent(buffer, indent, compressed)?;
112 }
113 Ok(())
114 }
115 Leaf::StatefulComponent(_comp) => {
116 write!(buffer, "<!-- stateful component -->")
117 }
118 Leaf::StatelessComponent(comp) => comp.view.render(buffer),
119 Leaf::TemplatedView(view) => view.view.render(buffer),
120 }
121 }
122}
123
124impl<MSG> Element<MSG> {
125 pub fn render_with_indent(
127 &self,
128 buffer: &mut dyn fmt::Write,
129 indent: usize,
130 compressed: bool,
131 ) -> fmt::Result {
132 write!(buffer, "<{}", self.tag())?;
133
134 let merged_attributes: Vec<Attribute<MSG>> =
135 Attribute::merge_attributes_of_same_name(self.attributes().iter());
136
137 for attr in &merged_attributes {
138 write!(buffer, " ")?;
139 attr.render(buffer)?;
140 }
141
142 if self.self_closing {
143 write!(buffer, "/>")?;
144 } else {
145 write!(buffer, ">")?;
146 }
147
148 let children = self.children();
149 let first_child = children.first();
150 let is_first_child_text_node = first_child.map(|node| node.is_text()).unwrap_or(false);
151
152 let is_lone_child_text_node = children.len() == 1 && is_first_child_text_node;
153
154 if is_lone_child_text_node {
156 first_child
157 .unwrap()
158 .render_with_indent(buffer, indent, compressed)?;
159 } else {
160 for child in self.children() {
162 maybe_indent(buffer, indent + 1, compressed)?;
163 child.render_with_indent(buffer, indent + 1, compressed)?;
164 }
165 }
166
167 if !is_lone_child_text_node && !children.is_empty() {
169 maybe_indent(buffer, indent, compressed)?;
170 }
171
172 if !self.self_closing {
173 write!(buffer, "</{}>", self.tag())?;
174 }
175 Ok(())
176 }
177}
178
179impl<MSG> Attribute<MSG> {
180 fn render(&self, buffer: &mut dyn fmt::Write) -> fmt::Result {
182 let GroupedAttributeValues {
183 plain_values,
184 styles,
185 ..
186 } = Attribute::group_values(self);
187
188 let boolean_attributes = ["open", "checked", "disabled"];
196
197 let bool_value: bool = plain_values
198 .first()
199 .and_then(|v| v.as_bool())
200 .unwrap_or(false);
201
202 let should_skip_attribute = boolean_attributes.contains(self.name()) && !bool_value;
204
205 if !should_skip_attribute {
206 if let Some(merged_plain_values) = Value::merge_to_string(plain_values) {
207 write!(buffer, "{}=\"{}\"", self.name(), merged_plain_values)?;
208 }
209 if let Some(merged_styles) = Style::merge_to_string(styles) {
210 write!(buffer, "{}=\"{}\"", self.name(), merged_styles)?;
211 }
212 }
213 Ok(())
214 }
215
216 pub fn render_to_string(&self) -> String {
218 let mut buffer = String::new();
219 self.render(&mut buffer).expect("must render");
220 buffer
221 }
222}
223
224#[cfg(test)]
225mod test {
226 use super::*;
227 use crate::html::{attributes::*, *};
228
229 #[test]
230 fn test_render_comments() {
231 let view: Node<()> = div(vec![], vec![comment("comment1"), comment("comment2")]);
232
233 assert_eq!(
234 view.render_to_string(),
235 "<div><!--comment1--><!--comment2--></div>"
236 );
237 }
238
239 #[test]
240 fn test_render_text_siblings_should_be_separated_with_comments() {
241 let view: Node<()> = div(vec![], vec![text("text1"), text("text2")]);
242
243 assert_eq!(
244 view.render_to_string(),
245 "<div>text1<!--separator-->text2</div>"
246 );
247 }
248
249 #[test]
250 fn test_render_classes() {
251 let view: Node<()> = div(vec![class("frame"), class("component")], vec![]);
252 let expected = r#"<div class="frame component"></div>"#;
253 let mut buffer = String::new();
254 view.render(&mut buffer).expect("must render");
255 assert_eq!(expected, buffer);
256 }
257
258 #[test]
259 fn test_render_class_flag() {
260 let view: Node<()> = div(
261 vec![
262 class("frame"),
263 classes_flag([("component", true), ("layer", false)]),
264 ],
265 vec![],
266 );
267 let expected = r#"<div class="frame component"></div>"#;
268 let mut buffer = String::new();
269 view.render(&mut buffer).expect("must render");
270 assert_eq!(expected, buffer);
271 }
272}