sauron_core/vdom/
render.rs

1//! This contains a trait to be able to render
2//! virtual dom into a writable buffer
3//!
4use 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
14/// add an indent if applicable
15fn 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    // ISSUE: sublte difference in `render` and `render_to_string`:
28    //  - flow content element such as span will treat the whitespace in between them as html text
29    //  node
30    //  Example:
31    //  in `render`
32    //  ```html
33    //     <span>hello</span>
34    //     <span> world</span>
35    //  ```
36    //     will displayed as "hello  world"
37    //
38    //  where us `render_to_string`
39    //  ```html
40    //  <span>hello</span><span> world</span>
41    //  ```
42    //  will result to a desirable output: "hello world"
43    //
44    /// render the node to a writable buffer
45    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    /// render the node to a writable buffer
58    pub fn render(&self, buffer: &mut dyn fmt::Write) -> fmt::Result {
59        self.render_with_indent(buffer, 0, false)
60    }
61
62    /// no new_lines, no indents
63    fn render_compressed(&self, buffer: &mut dyn fmt::Write) -> fmt::Result {
64        self.render_with_indent(buffer, 0, true)
65    }
66
67    /// render compressed html to string
68    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    /// render to string with nice indention
75    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    /// render leaf nodes
84    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    /// render element nodes
126    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        // do not indent if it is only text child node
155        if is_lone_child_text_node {
156            first_child
157                .unwrap()
158                .render_with_indent(buffer, indent, compressed)?;
159        } else {
160            // otherwise print all child nodes with each line and indented
161            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        // do not make a new line it if is only a text child node or it has no child nodes
168        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    /// render attributes
181    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        // These are attribute values which specifies the state of the element
189        // regardless of it's value.
190        // This is counter-intuitive to what we are trying to do, therefore
191        // we use something that if the value is false, we skip the attribute from being part
192        // of the render which then satisfies our intent to the the browser behavior.
193        //
194        // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled
195        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        // skip this attribute if the boolean attributes evaluates to false
203        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    /// render compressed html to string
217    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}