Skip to main content

easy_svg/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::fmt::{Display, Formatter, Write};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5/// Controls how an SVG element is rendered to a string.
6pub struct FormatOptions {
7    indent: Option<String>,
8}
9
10impl FormatOptions {
11    /// Renders without insignificant whitespace.
12    #[must_use]
13    pub const fn compact() -> Self {
14        Self { indent: None }
15    }
16
17    /// Renders element children on separate lines with two-space indentation.
18    #[must_use]
19    pub fn pretty() -> Self {
20        Self::pretty_with_indent("  ")
21    }
22
23    /// Renders element children on separate lines with the given indentation.
24    #[must_use]
25    pub fn pretty_with_indent(indent: impl Into<String>) -> Self {
26        Self {
27            indent: Some(indent.into()),
28        }
29    }
30}
31
32impl Default for FormatOptions {
33    fn default() -> Self {
34        Self::compact()
35    }
36}
37
38#[doc(hidden)]
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct ElementData {
41    attributes: Vec<(String, String)>,
42    children: Vec<Node>,
43}
44
45impl ElementData {
46    pub fn set_attribute(&mut self, name: impl Into<String>, value: impl Display) {
47        let name = name.into();
48        let value = value.to_string();
49        if let Some((_, current)) = self.attributes.iter_mut().find(|(key, _)| key == &name) {
50            *current = value;
51        } else {
52            self.attributes.push((name, value));
53        }
54    }
55
56    pub fn push_child(&mut self, child: impl Into<Node>) {
57        self.children.push(child.into());
58    }
59}
60
61#[doc(hidden)]
62pub fn render_element(name: &str, data: &ElementData, f: &mut Formatter<'_>) -> std::fmt::Result {
63    write!(f, "<{name}")?;
64    for (attribute, value) in &data.attributes {
65        write!(f, " {attribute}=\"{}\"", escape_attribute(value))?;
66    }
67    if data.children.is_empty() {
68        return f.write_str("/>");
69    }
70    f.write_str(">")?;
71    for child in &data.children {
72        write!(f, "{child}")?;
73    }
74    write!(f, "</{name}>")
75}
76
77#[doc(hidden)]
78pub fn render_element_with_options(
79    output: &mut String,
80    name: &str,
81    data: &ElementData,
82    options: &FormatOptions,
83    depth: usize,
84) {
85    if let Some(indent) = &options.indent {
86        output.push_str(&indent.repeat(depth));
87    }
88    write!(output, "<{name}").expect("writing to String cannot fail");
89    for (attribute, value) in &data.attributes {
90        write!(output, " {attribute}=\"{}\"", escape_attribute(value))
91            .expect("writing to String cannot fail");
92    }
93    if data.children.is_empty() {
94        output.push_str("/>");
95        return;
96    }
97
98    output.push('>');
99    let pretty_children =
100        options.indent.is_some() && data.children.iter().all(Node::supports_pretty_indentation);
101    if pretty_children {
102        output.push('\n');
103        for (index, child) in data.children.iter().enumerate() {
104            child.render_with_options(output, options, depth + 1);
105            if index + 1 != data.children.len() {
106                output.push('\n');
107            }
108        }
109        output.push('\n');
110        output.push_str(&options.indent.as_deref().unwrap_or_default().repeat(depth));
111    } else {
112        let compact = FormatOptions::compact();
113        for child in &data.children {
114            child.render_with_options(output, &compact, 0);
115        }
116    }
117    write!(output, "</{name}>").expect("writing to String cannot fail");
118}
119
120fn escape_attribute(value: &str) -> String {
121    value
122        .replace('&', "&amp;")
123        .replace('"', "&quot;")
124        .replace('<', "&lt;")
125}
126
127fn escape_text(value: &str) -> String {
128    value.replace('&', "&amp;").replace('<', "&lt;")
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Raw(String);
133
134impl Raw {
135    pub fn new(svg: impl Into<String>) -> Self {
136        Self(svg.into())
137    }
138}
139
140impl Display for Raw {
141    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142        f.write_str(&self.0)
143    }
144}
145
146#[allow(clippy::nursery, clippy::pedantic)]
147pub mod generated {
148    include!(concat!(env!("OUT_DIR"), "/generated.rs"));
149}
150
151pub use generated::types;
152pub use generated::*;