Skip to main content

use_svg/
normalize.rs

1use crate::document::strip_xml_declaration;
2
3#[must_use]
4pub fn strip_comments(input: &str) -> String {
5    let mut cleaned = String::with_capacity(input.len());
6    let mut index = 0;
7
8    while let Some(start) = input[index..].find("<!--") {
9        let absolute_start = index + start;
10        cleaned.push_str(&input[index..absolute_start]);
11
12        let Some(end) = input[absolute_start + 4..].find("-->") else {
13            return cleaned;
14        };
15
16        index = absolute_start + 4 + end + 3;
17    }
18
19    cleaned.push_str(&input[index..]);
20    cleaned
21}
22
23#[must_use]
24pub fn normalize_svg(input: &str) -> String {
25    let without_comments = strip_comments(strip_xml_declaration(input));
26    let trimmed = without_comments.trim();
27
28    if trimmed.is_empty() {
29        return String::new();
30    }
31
32    normalize_tag_whitespace(trimmed)
33}
34
35#[must_use]
36pub fn minify_svg_basic(input: &str) -> String {
37    let normalized = normalize_svg(input);
38
39    if normalized.is_empty() {
40        return normalized;
41    }
42
43    remove_intertag_whitespace(&normalized)
44}
45
46fn normalize_tag_whitespace(input: &str) -> String {
47    let mut normalized = String::with_capacity(input.len());
48    let mut inside_tag = false;
49    let mut active_quote = None;
50    let mut pending_space = false;
51    let mut suppress_space = false;
52
53    for ch in input.chars() {
54        if let Some(quote) = active_quote {
55            normalized.push(ch);
56            if ch == quote {
57                active_quote = None;
58            }
59            continue;
60        }
61
62        if inside_tag {
63            match ch {
64                '<' => normalized.push(ch),
65                '"' | '\'' => {
66                    if pending_space
67                        && !suppress_space
68                        && !normalized.ends_with('<')
69                        && !normalized.ends_with('=')
70                    {
71                        normalized.push(' ');
72                    }
73                    pending_space = false;
74                    suppress_space = false;
75                    normalized.push(ch);
76                    active_quote = Some(ch);
77                },
78                '>' => {
79                    if normalized.ends_with(' ') {
80                        normalized.pop();
81                    }
82                    normalized.push('>');
83                    inside_tag = false;
84                    pending_space = false;
85                    suppress_space = false;
86                },
87                '=' => {
88                    if normalized.ends_with(' ') {
89                        normalized.pop();
90                    }
91                    normalized.push('=');
92                    pending_space = false;
93                    suppress_space = true;
94                },
95                '/' => {
96                    if normalized.ends_with(' ') {
97                        normalized.pop();
98                    }
99                    normalized.push('/');
100                    pending_space = false;
101                    suppress_space = false;
102                },
103                _ if ch.is_whitespace() => pending_space = true,
104                _ => {
105                    if pending_space
106                        && !suppress_space
107                        && !normalized.ends_with('<')
108                        && !normalized.ends_with('/')
109                        && !normalized.ends_with('=')
110                    {
111                        normalized.push(' ');
112                    }
113                    normalized.push(ch);
114                    pending_space = false;
115                    suppress_space = false;
116                },
117            }
118        } else if ch == '<' {
119            inside_tag = true;
120            pending_space = false;
121            suppress_space = false;
122            normalized.push(ch);
123        } else {
124            normalized.push(ch);
125        }
126    }
127
128    normalized
129}
130
131fn remove_intertag_whitespace(input: &str) -> String {
132    let chars: Vec<_> = input.chars().collect();
133    let mut minified = String::with_capacity(input.len());
134    let mut index = 0;
135
136    while index < chars.len() {
137        if chars[index].is_whitespace() {
138            let mut next = index;
139            while next < chars.len() && chars[next].is_whitespace() {
140                next += 1;
141            }
142
143            let previous = minified.chars().last();
144            let following = chars.get(next).copied();
145
146            if previous == Some('>') && following == Some('<') {
147                index = next;
148                continue;
149            }
150
151            for ch in &chars[index..next] {
152                minified.push(*ch);
153            }
154            index = next;
155            continue;
156        }
157
158        minified.push(chars[index]);
159        index += 1;
160    }
161
162    minified.trim().to_string()
163}