note_mark/layer/
stringifier.rs1use crate::model::html::*;
4
5#[derive(Debug, Clone)]
9pub struct Stringifier {
10 pub format: bool,
12 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 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn format(mut self, format: bool) -> Self {
33 self.format = format;
34 self
35 }
36
37 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 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}