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}