flou/svg/
element.rs

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("&apos;"),
18                '"' => output.push_str("&quot;"),
19                '<' => output.push_str("&lt;"),
20                '>' => output.push_str("&gt;"),
21                '&' => output.push_str("&amp;"),
22                _ => output.push(c),
23            }
24        }
25        Cow::Owned(output)
26    } else {
27        Cow::Borrowed(input)
28    }
29}
30
31// This is a hacky workaround for lifetime issues in SVGElement::text().
32// There's probably a better way of resolving them without duplicating code.
33fn 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("&apos;"),
43                '"' => output.push_str("&quot;"),
44                '<' => output.push_str("&lt;"),
45                '>' => output.push_str("&gt;"),
46                '&' => output.push_str("&amp;"),
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("\""), "&quot;");
274        assert_eq!(escape("'"), "&apos;");
275        assert_eq!(escape("<"), "&lt;");
276        assert_eq!(escape(">"), "&gt;");
277        assert_eq!(escape("&"), "&amp;");
278    }
279
280    #[test]
281    fn with_escaped_attribute() {
282        assert_eq!(
283            SVGElement::new("div").class("'Hi'").to_string(),
284            r#"<div class="&apos;Hi&apos;" />"#,
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}