1use std::collections::HashMap;
2
3use crate::passes::{self, Pass};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum Preset {
8 Safe,
10 #[default]
12 Default,
13}
14
15#[derive(Debug, Clone, Default)]
17pub struct Config {
18 pub preset: Preset,
20 pub precision: Option<u32>,
23 pub pass_overrides: HashMap<String, bool>,
25}
26
27impl Config {
28 pub fn effective_precision(&self) -> u32 {
30 self.precision.unwrap_or(3)
31 }
32}
33
34const PASS_CATALOG: &[(&str, bool, bool)] = &[
36 ("removeDoctype", true, true),
37 ("removeProcInst", true, true),
38 ("removeComments", true, true),
39 ("removeDeprecatedAttrs", true, true),
40 ("removeMetadata", true, true),
41 ("removeEditorData", true, true),
42 ("removeDesc", false, true),
43 ("removeEmptyAttrs", true, true),
44 ("removeEmptyText", true, true),
45 ("removeHiddenElems", true, true),
46 ("removeUselessDefs", false, true),
47 ("removeUselessStrokeAndFill", false, true),
48 ("removeEmptyContainers", true, true),
49 ("removeUnusedNamespaces", true, true),
50 ("cleanupAttrs", true, true),
51 ("inlineStyles", false, true),
52 ("minifyStyles", true, true),
53 ("cleanupNumericValues", true, true),
54 ("convertColors", true, true),
55 ("removeUnknownsAndDefaults", true, true),
56 ("removeNonInheritableGroupAttrs", false, true),
57 ("cleanupEnableBackground", true, true),
58 ("convertEllipseToCircle", false, true),
59 ("convertShapeToPath", false, true),
60 ("moveElemsAttrsToGroup", false, true),
61 ("moveGroupAttrsToElems", false, true),
62 ("convertTransform", false, true),
63 ("collapseGroups", false, true),
64 ("cleanupIds", false, true),
65 ("convertPathData", false, true),
66 ("mergePaths", false, true),
67 ("sortAttrs", true, true),
68 ("sortDefsChildren", true, true),
69 ("minifyWhitespace", true, true),
70];
71
72fn is_in_preset(entry: &(&str, bool, bool), preset: Preset) -> bool {
73 match preset {
74 Preset::Safe => entry.1,
75 Preset::Default => entry.2,
76 }
77}
78
79fn create_pass(name: &str, precision: u32) -> Box<dyn Pass> {
80 match name {
81 "removeDoctype" => Box::new(passes::remove_doctype::RemoveDoctype),
82 "removeProcInst" => Box::new(passes::remove_proc_inst::RemoveProcInst),
83 "removeComments" => Box::new(passes::remove_comments::RemoveComments),
84 "removeDeprecatedAttrs" => Box::new(passes::remove_deprecated_attrs::RemoveDeprecatedAttrs),
85 "removeMetadata" => Box::new(passes::remove_metadata::RemoveMetadata),
86 "removeEditorData" => Box::new(passes::remove_editor_data::RemoveEditorData),
87 "removeDesc" => Box::new(passes::remove_desc::RemoveDesc),
88 "removeEmptyAttrs" => Box::new(passes::remove_empty_attrs::RemoveEmptyAttrs),
89 "removeEmptyText" => Box::new(passes::remove_empty_text::RemoveEmptyText),
90 "removeHiddenElems" => Box::new(passes::remove_hidden_elems::RemoveHiddenElems),
91 "removeUselessDefs" => Box::new(passes::remove_useless_defs::RemoveUselessDefs),
92 "removeUselessStrokeAndFill" => {
93 Box::new(passes::remove_useless_stroke_and_fill::RemoveUselessStrokeAndFill)
94 }
95 "removeEmptyContainers" => Box::new(passes::remove_empty_containers::RemoveEmptyContainers),
96 "removeUnusedNamespaces" => {
97 Box::new(passes::remove_unused_namespaces::RemoveUnusedNamespaces)
98 }
99 "cleanupAttrs" => Box::new(passes::cleanup_attrs::CleanupAttrs),
100 "inlineStyles" => Box::new(passes::inline_styles::InlineStyles),
101 "minifyStyles" => Box::new(passes::minify_styles::MinifyStyles),
102 "cleanupNumericValues" => {
103 Box::new(passes::cleanup_numeric_values::CleanupNumericValues { precision })
104 }
105 "convertColors" => Box::new(passes::convert_colors::ConvertColors),
106 "removeUnknownsAndDefaults" => {
107 Box::new(passes::remove_unknowns_and_defaults::RemoveUnknownsAndDefaults)
108 }
109 "removeNonInheritableGroupAttrs" => {
110 Box::new(passes::remove_non_inheritable_group_attrs::RemoveNonInheritableGroupAttrs)
111 }
112 "cleanupEnableBackground" => {
113 Box::new(passes::cleanup_enable_background::CleanupEnableBackground)
114 }
115 "convertEllipseToCircle" => {
116 Box::new(passes::convert_ellipse_to_circle::ConvertEllipseToCircle)
117 }
118 "convertShapeToPath" => {
119 Box::new(passes::convert_shape_to_path::ConvertShapeToPath { precision })
120 }
121 "moveElemsAttrsToGroup" => {
122 Box::new(passes::move_elems_attrs_to_group::MoveElemsAttrsToGroup)
123 }
124 "moveGroupAttrsToElems" => {
125 Box::new(passes::move_group_attrs_to_elems::MoveGroupAttrsToElems)
126 }
127 "convertTransform" => Box::new(passes::convert_transform::ConvertTransform { precision }),
128 "collapseGroups" => Box::new(passes::collapse_groups::CollapseGroups),
129 "cleanupIds" => Box::new(passes::cleanup_ids::CleanupIds),
130 "convertPathData" => Box::new(passes::convert_path_data::ConvertPathData { precision }),
131 "mergePaths" => Box::new(passes::merge_paths::MergePaths),
132 "sortAttrs" => Box::new(passes::sort_attrs::SortAttrs),
133 "sortDefsChildren" => Box::new(passes::sort_defs_children::SortDefsChildren),
134 "minifyWhitespace" => Box::new(passes::minify_whitespace::MinifyWhitespace),
135 _ => panic!("unknown pass: {name}"),
136 }
137}
138
139pub fn passes_for_config(config: &Config) -> Vec<Box<dyn Pass>> {
141 let precision = config.effective_precision();
142 let mut result = Vec::new();
143
144 for entry in PASS_CATALOG {
145 let name = entry.0;
146 let enabled = if let Some(&override_val) = config.pass_overrides.get(name) {
147 override_val
148 } else {
149 is_in_preset(entry, config.preset)
150 };
151
152 if enabled {
153 result.push(create_pass(name, precision));
154 }
155 }
156
157 result
158}
159
160pub fn all_pass_names() -> Vec<&'static str> {
162 PASS_CATALOG.iter().map(|e| e.0).collect()
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn pass_names(config: &Config) -> Vec<&'static str> {
170 passes_for_config(config).iter().map(|p| p.name()).collect()
171 }
172
173 #[test]
174 fn safe_preset_passes() {
175 let config = Config {
176 preset: Preset::Safe,
177 ..Config::default()
178 };
179 let names = pass_names(&config);
180 assert_eq!(
181 names,
182 vec![
183 "removeDoctype",
184 "removeProcInst",
185 "removeComments",
186 "removeDeprecatedAttrs",
187 "removeMetadata",
188 "removeEditorData",
189 "removeEmptyAttrs",
190 "removeEmptyText",
191 "removeHiddenElems",
192 "removeEmptyContainers",
193 "removeUnusedNamespaces",
194 "cleanupAttrs",
195 "minifyStyles",
196 "cleanupNumericValues",
197 "convertColors",
198 "removeUnknownsAndDefaults",
199 "cleanupEnableBackground",
200 "sortAttrs",
201 "sortDefsChildren",
202 "minifyWhitespace",
203 ]
204 );
205 assert_eq!(names.len(), 20);
206 }
207
208 #[test]
209 fn default_preset_passes() {
210 let config = Config::default();
211 assert_eq!(config.preset, Preset::Default);
212 let names = pass_names(&config);
213 assert_eq!(
214 names,
215 vec![
216 "removeDoctype",
217 "removeProcInst",
218 "removeComments",
219 "removeDeprecatedAttrs",
220 "removeMetadata",
221 "removeEditorData",
222 "removeDesc",
223 "removeEmptyAttrs",
224 "removeEmptyText",
225 "removeHiddenElems",
226 "removeUselessDefs",
227 "removeUselessStrokeAndFill",
228 "removeEmptyContainers",
229 "removeUnusedNamespaces",
230 "cleanupAttrs",
231 "inlineStyles",
232 "minifyStyles",
233 "cleanupNumericValues",
234 "convertColors",
235 "removeUnknownsAndDefaults",
236 "removeNonInheritableGroupAttrs",
237 "cleanupEnableBackground",
238 "convertEllipseToCircle",
239 "convertShapeToPath",
240 "moveElemsAttrsToGroup",
241 "moveGroupAttrsToElems",
242 "convertTransform",
243 "collapseGroups",
244 "cleanupIds",
245 "convertPathData",
246 "mergePaths",
247 "sortAttrs",
248 "sortDefsChildren",
249 "minifyWhitespace",
250 ]
251 );
252 assert_eq!(names.len(), 34);
253 }
254
255 #[test]
256 fn override_enables_pass_not_in_preset() {
257 let config = Config {
259 preset: Preset::Safe,
260 pass_overrides: HashMap::from([("removeDesc".to_string(), true)]),
261 ..Config::default()
262 };
263 let names = pass_names(&config);
264 assert!(names.contains(&"removeDesc"));
265 }
266
267 #[test]
268 fn override_disables_preset_pass() {
269 let config = Config {
270 preset: Preset::Default,
271 pass_overrides: HashMap::from([("collapseGroups".to_string(), false)]),
272 ..Config::default()
273 };
274 let names = pass_names(&config);
275 assert!(!names.contains(&"collapseGroups"));
276 }
277
278 #[test]
279 fn effective_precision_defaults() {
280 assert_eq!(
281 Config {
282 preset: Preset::Safe,
283 ..Config::default()
284 }
285 .effective_precision(),
286 3
287 );
288 assert_eq!(Config::default().effective_precision(), 3);
289 }
290
291 #[test]
292 fn explicit_precision_overrides_default() {
293 let config = Config {
294 precision: Some(4),
295 ..Config::default()
296 };
297 assert_eq!(config.effective_precision(), 4);
298 }
299}