note_mark/layer/
stringifier.rs

1//! Stringify DocumentNode to html string.
2
3use crate::model::html::*;
4
5/// Stringify DocumentNode to html string.
6///
7/// This contains some options.
8#[derive(Debug, Clone)]
9pub struct Stringifier {
10    /// Whether to format the output. Default is false.
11    pub format: bool,
12    /// The width of the line to break the code and indent. Default is 20.
13    pub width: u32,
14}
15
16impl Default for Stringifier {
17    fn default() -> Self {
18        Self {
19            format: false,
20            width: 20,
21        }
22    }
23}
24
25impl Stringifier {
26    /// Create a new Stringifier.
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Set whether to format the output.
32    pub fn format(mut self, format: bool) -> Self {
33        self.format = format;
34        self
35    }
36
37    /// Set the width of the output.
38    pub fn width(mut self, width: u32) -> Self {
39        self.width = width;
40        self
41    }
42}
43
44fn tag_to_str(tag: ElementTag) -> &'static str {
45    match tag {
46        ElementTag::Div => "div",
47        ElementTag::Span => "span",
48        ElementTag::P => "p",
49        ElementTag::H1 => "h1",
50        ElementTag::H2 => "h2",
51        ElementTag::H3 => "h3",
52        ElementTag::H4 => "h4",
53        ElementTag::H5 => "h5",
54        ElementTag::H6 => "h6",
55        ElementTag::Ul => "ul",
56        ElementTag::Ol => "ol",
57        ElementTag::Li => "li",
58        ElementTag::Blockquote => "blockquote",
59        ElementTag::A => "a",
60        ElementTag::Strong => "strong",
61        ElementTag::Em => "em",
62        ElementTag::Br => "br",
63    }
64}
65
66impl Stringifier {
67    /// Stringify DocumentNode to html string.
68    pub fn stringify(&self, document: DocumentNode) -> String {
69        let list = document
70            .root
71            .into_iter()
72            .map(|node| self.stringify_node(node))
73            .collect::<Vec<_>>();
74
75        if self.format {
76            list.join("\n")
77        } else {
78            list.join("")
79        }
80    }
81
82    fn stringify_node(&self, node: Node) -> String {
83        match node {
84            Node::Element(element) => self.stringify_element(element),
85            Node::Text(text) => self.stringify_text(text),
86        }
87    }
88
89    fn stringify_element(&self, element: ElementNode) -> String {
90        let tag = tag_to_str(element.tag);
91
92        match element.tag {
93            ElementTag::Br => format!("<{tag}>"),
94            _ => {
95                let mut attrs = String::new();
96
97                if !element.class.is_empty() {
98                    attrs += &format!(
99                        " class=\"{}\"",
100                        element.class.into_iter().collect::<Vec<_>>().join(" ")
101                    );
102                }
103
104                if !element.id.is_empty() {
105                    attrs += &format!(
106                        " id=\"{}\"",
107                        element.id.into_iter().collect::<Vec<_>>().join(" ")
108                    );
109                }
110
111                if let Some(href) = element.href {
112                    attrs += &format!(" href=\"{href}\"");
113                }
114
115                attrs += &element
116                    .attrs
117                    .iter()
118                    .map(|(name, value)| format!(" {name}=\"{value}\""))
119                    .collect::<String>();
120
121                let list = element
122                    .children
123                    .iter()
124                    .cloned()
125                    .map(|node| self.stringify_node(node))
126                    .collect::<Vec<_>>();
127
128                let inner = if self.format {
129                    if element.children.len() == 1 {
130                        let child = list[0].clone();
131
132                        if child.len() >= self.width as usize {
133                            let child = Self::add_indent(&child);
134
135                            format!("\n{child}\n")
136                        } else {
137                            child
138                        }
139                    } else if !element.children.iter().any(|node| node.is_block_item()) {
140                        list.join("")
141                    } else {
142                        let children = list.join("\n");
143
144                        let children = Self::add_indent(&children);
145
146                        format!("\n{children}\n")
147                    }
148                } else {
149                    list.join("")
150                };
151
152                format!("<{tag}{attrs}>{inner}</{tag}>")
153            }
154        }
155    }
156
157    fn stringify_text(&self, text: TextNode) -> String {
158        text.text.to_string()
159    }
160
161    fn add_indent(input: &str) -> String {
162        input
163            .lines()
164            .map(|line| String::from("    ") + line)
165            .collect::<Vec<_>>()
166            .join("\n")
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_stringify() {
176        let document = DocumentNode {
177            root: vec![Node::Element(ElementNode {
178                tag: ElementTag::P,
179                children: vec![Node::Text(TextNode {
180                    text: "Hello, world!".into(),
181                })],
182                ..Default::default()
183            })],
184        };
185
186        let stringifier = Stringifier::new();
187
188        assert_eq!(
189            stringifier.stringify(document),
190            "<p>Hello, world!</p>".to_string()
191        );
192
193        let document = DocumentNode {
194            root: vec![Node::Element(ElementNode {
195                tag: ElementTag::P,
196                children: vec![
197                    Node::Text(TextNode {
198                        text: "Hello, ".into(),
199                    }),
200                    Node::Element(ElementNode {
201                        tag: ElementTag::Strong,
202                        children: vec![Node::Text(TextNode {
203                            text: "world".into(),
204                        })],
205                        ..Default::default()
206                    }),
207                    Node::Text(TextNode { text: "!".into() }),
208                    Node::Element(ElementNode {
209                        tag: ElementTag::Br,
210                        ..Default::default()
211                    }),
212                    Node::Text(TextNode {
213                        text: "Hello, ".into(),
214                    }),
215                    Node::Element(ElementNode {
216                        tag: ElementTag::Strong,
217                        children: vec![Node::Text(TextNode {
218                            text: "world".into(),
219                        })],
220                        ..Default::default()
221                    }),
222                    Node::Text(TextNode { text: "!".into() }),
223                ],
224                ..Default::default()
225            })],
226        };
227
228        assert_eq!(
229            stringifier.stringify(document),
230            "<p>Hello, <strong>world</strong>!<br>Hello, <strong>world</strong>!</p>".to_string()
231        );
232    }
233
234    #[test]
235    fn test_stringify_attrs() {
236        let document = DocumentNode {
237            root: vec![Node::Element(ElementNode {
238                tag: ElementTag::P,
239                class: vec!["test".into(), "test2".into()],
240                id: vec!["ttt".into()],
241                href: Some("https://example.com".into()),
242                attrs: vec![
243                    ("data-test".into(), "ok".into()),
244                    ("data-test2".into(), "ok2".into()),
245                ],
246                children: vec![Node::Text(TextNode {
247                    text: "Hello, world!".into(),
248                })],
249                ..Default::default()
250            })],
251        };
252
253        let stringifier = Stringifier::new();
254
255        assert_eq!(
256            stringifier.stringify(document),
257            "<p class=\"test test2\" id=\"ttt\" href=\"https://example.com\" data-test=\"ok\" data-test2=\"ok2\">Hello, world!</p>".to_string()
258        );
259    }
260}