1use std::{
2 borrow::Cow,
3 fmt::{self},
4};
5
6use crate::pos::PixelPos;
7
8fn escape(input: &str) -> Cow<str> {
9 fn should_escape(c: char) -> bool {
10 c == '<' || c == '>' || c == '&' || c == '"' || c == '\''
11 }
12
13 if input.contains(should_escape) {
14 let mut output = String::with_capacity(input.len());
15 for c in input.chars() {
16 match c {
17 '\'' => output.push_str("'"),
18 '"' => output.push_str("""),
19 '<' => output.push_str("<"),
20 '>' => output.push_str(">"),
21 '&' => output.push_str("&"),
22 _ => output.push(c),
23 }
24 }
25 Cow::Owned(output)
26 } else {
27 Cow::Borrowed(input)
28 }
29}
30
31fn escape_cow(input: Cow<str>) -> Cow<str> {
34 fn should_escape(c: char) -> bool {
35 c == '<' || c == '>' || c == '&' || c == '"' || c == '\''
36 }
37
38 if input.contains(should_escape) {
39 let mut output = String::with_capacity(input.len());
40 for c in input.chars() {
41 match c {
42 '\'' => output.push_str("'"),
43 '"' => output.push_str("""),
44 '<' => output.push_str("<"),
45 '>' => output.push_str(">"),
46 '&' => output.push_str("&"),
47 _ => output.push(c),
48 }
49 }
50 Cow::Owned(output)
51 } else {
52 input
53 }
54}
55
56fn indent(depth: usize) -> String {
57 const SIZE: usize = 2;
58 " ".repeat(SIZE * depth)
59}
60
61#[derive(Debug)]
62enum Node<'a> {
63 Text(Cow<'a, str>),
64 Element(SVGElement<'a>),
65}
66
67impl Node<'_> {
68 fn print(&self, depth: usize, f: &mut fmt::Formatter) -> fmt::Result {
69 match self {
70 Node::Text(text) => {
71 for (i, line) in text.lines().enumerate() {
72 if i != 0 {
73 writeln!(f)?;
74 }
75 f.write_str(&indent(depth))?;
76 f.write_str(line)?;
77 }
78 }
79 Node::Element(el) => el.print(depth, f)?,
80 };
81
82 Ok(())
83 }
84}
85
86#[derive(Debug)]
87pub(crate) struct SVGElement<'a> {
88 tag: Cow<'a, str>,
89 attributes: Vec<(Cow<'a, str>, Cow<'a, str>)>,
90 classes: Vec<Cow<'a, str>>,
91 children: Vec<Node<'a>>,
92}
93
94impl<'a> SVGElement<'a> {
95 pub(crate) fn new<I: Into<Cow<'a, str>>>(tag: I) -> Self {
96 Self {
97 tag: tag.into(),
98 attributes: Vec::new(),
99 classes: Vec::new(),
100 children: Vec::new(),
101 }
102 }
103
104 pub(crate) fn pos(self, pos: PixelPos) -> Self {
105 self.attr("x", pos.x.to_string())
106 .attr("y", pos.y.to_string())
107 }
108
109 pub(crate) fn cpos(self, pos: PixelPos) -> Self {
110 self.attr("cx", pos.x.to_string())
111 .attr("cy", pos.y.to_string())
112 }
113
114 pub(crate) fn size(self, size: PixelPos) -> Self {
115 self.attr("width", size.x.to_string())
116 .attr("height", size.y.to_string())
117 }
118
119 pub(crate) fn class<I: Into<Cow<'a, str>>>(mut self, s: I) -> Self {
120 self.classes.push(s.into());
121 self
122 }
123
124 pub(crate) fn class_opt<I: Into<Cow<'a, str>>>(self, s: Option<I>) -> Self {
125 match s {
126 Some(s) => self.class(s),
127 None => self,
128 }
129 }
130
131 pub(crate) fn attr<K, V>(mut self, key: K, value: V) -> Self
132 where
133 K: Into<Cow<'a, str>>,
134 V: Into<Cow<'a, str>>,
135 {
136 let key = key.into();
137 if key == "class" {
138 panic!("Use .class() instead.");
139 }
140
141 self.attributes.push((key, value.into()));
142 self
143 }
144
145 pub(crate) fn child(mut self, child: SVGElement<'a>) -> Self {
146 self.children.push(Node::Element(child));
147 self
148 }
149
150 pub(crate) fn child_opt(self, child: Option<SVGElement<'a>>) -> Self {
151 match child {
152 Some(child) => self.child(child),
153 None => self,
154 }
155 }
156
157 pub(crate) fn text<I: Into<Cow<'a, str>>>(mut self, text: I) -> Self {
158 let text = text.into();
159 let text = escape_cow(text);
160 self.children.push(Node::Text(text));
161 self
162 }
163
164 pub(crate) fn children<T>(mut self, children: T) -> Self
165 where
166 T: IntoIterator<Item = SVGElement<'a>>,
167 {
168 self.children
169 .extend(children.into_iter().map(Node::Element));
170 self
171 }
172
173 fn print(&self, depth: usize, f: &mut fmt::Formatter) -> fmt::Result {
174 let attributes = self
175 .attributes
176 .iter()
177 .map(|(key, value)| (key, escape(value)))
178 .collect::<Vec<_>>();
179
180 let classes = self.classes.iter().map(|x| escape(x)).collect::<Vec<_>>();
181
182 f.write_str(&indent(depth))?;
183 f.write_str("<")?;
184 f.write_str(&self.tag)?;
185
186 if !classes.is_empty() {
187 write!(f, " class=\"{}\"", classes.join(" "))?;
188 }
189
190 let attributes = attributes
191 .iter()
192 .map(|(k, v)| format!(" {}=\"{}\"", k, v))
193 .collect::<Vec<_>>()
194 .join("");
195 f.write_str(&attributes)?;
196
197 if self.children.is_empty() {
198 f.write_str(" />")?;
199 return Ok(());
200 }
201
202 f.write_str(">")?;
203
204 match self.children.first() {
205 Some(child @ Node::Text(_)) if self.children.len() == 1 => {
206 child.print(0, f)?;
207 }
208 _ => {
209 for child in &self.children {
210 writeln!(f)?;
211 child.print(depth + 1, f)?;
212 }
213 writeln!(f)?;
214 f.write_str(&indent(depth))?;
215 }
216 }
217
218 write!(f, "</{}>", self.tag)?;
219 Ok(())
220 }
221}
222
223impl fmt::Display for SVGElement<'_> {
224 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225 self.print(0, f)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::{escape, SVGElement};
232
233 use crate::test::assert_eq;
234
235 #[test]
236 fn tag_only() {
237 assert_eq!(SVGElement::new("a").to_string(), "<a />");
238 }
239
240 #[test]
241 fn with_attributes() {
242 assert_eq!(
243 SVGElement::new("a").attr("foo", "bar").to_string(),
244 r#"<a foo="bar" />"#,
245 );
246
247 assert_eq!(
248 SVGElement::new("a")
249 .attr("foo", "bar")
250 .attr("bar", "baz")
251 .to_string(),
252 r#"<a foo="bar" bar="baz" />"#,
253 );
254 }
255
256 #[test]
257 fn with_child() {
258 assert_eq!(
259 SVGElement::new("div")
260 .child(SVGElement::new("foo"))
261 .to_string(),
262 r#"
263<div>
264 <foo />
265</div>
266 "#
267 .trim(),
268 );
269 }
270
271 #[test]
272 fn escape_attributes() {
273 assert_eq!(escape("\""), """);
274 assert_eq!(escape("'"), "'");
275 assert_eq!(escape("<"), "<");
276 assert_eq!(escape(">"), ">");
277 assert_eq!(escape("&"), "&");
278 }
279
280 #[test]
281 fn with_escaped_attribute() {
282 assert_eq!(
283 SVGElement::new("div").class("'Hi'").to_string(),
284 r#"<div class="'Hi'" />"#,
285 )
286 }
287
288 #[test]
289 fn complex_example() {
290 assert_eq!(
291 SVGElement::new("p")
292 .class("block")
293 .child(
294 SVGElement::new("a")
295 .attr("href", "example.com")
296 .child(SVGElement::new("span").text("Hi"))
297 .text("there")
298 )
299 .child(SVGElement::new("button").text("Press me"))
300 .to_string(),
301 r#"
302<p class="block">
303 <a href="example.com">
304 <span>Hi</span>
305 there
306 </a>
307 <button>Press me</button>
308</p>
309 "#
310 .trim(),
311 );
312 }
313}