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}