Skip to main content

vexy_vsvg/parser/
config.rs

1// this_file: crates/vexy-vsvg/src/parser/config.rs
2
3//! Configuration for SVG optimization and parsing
4
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "json-schema")]
7use serde_json::Value;
8
9/// Configuration for streaming parser
10#[derive(Debug, Clone)]
11pub struct StreamingConfig {
12    /// Buffer size for reading (default: 64KB)
13    pub buffer_size: usize,
14    /// Maximum nesting depth to prevent stack overflow (default: 256)
15    pub max_depth: usize,
16    /// Enable lazy loading for large documents
17    pub lazy_loading: bool,
18    /// Threshold for large text nodes (bytes)
19    pub large_text_threshold: Option<usize>,
20    /// Maximum memory usage in bytes (approximate)
21    pub memory_limit: Option<usize>,
22}
23
24impl Default for StreamingConfig {
25    fn default() -> Self {
26        Self {
27            buffer_size: 64 * 1024, // 64KB
28            max_depth: 256,
29            lazy_loading: false,
30            large_text_threshold: None,
31            memory_limit: Some(100 * 1024 * 1024), // 100MB default limit
32        }
33    }
34}
35
36/// Main configuration structure
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38#[serde(default)]
39pub struct Config {
40    /// Multipass optimization
41    pub multipass: bool,
42
43    /// Pretty print output
44    pub pretty: bool,
45
46    /// Indent string for pretty printing
47    pub indent: String,
48
49    /// Plugin configurations
50    pub plugins: Vec<PluginConfig>,
51
52    /// JS2SVG output options
53    pub js2svg: Js2SvgOptions,
54
55    /// Data URI format (if specified)
56    pub datauri: Option<DataUriFormat>,
57
58    /// Parallel plugin execution (number of threads)
59    #[serde(skip)]
60    pub parallel: Option<usize>,
61
62    /// File path (used by CLI)
63    #[serde(skip)]
64    pub path: Option<String>,
65}
66
67/// Plugin configuration
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum PluginConfig {
71    /// Just the plugin name (enabled with default params)
72    Name(String),
73
74    /// Plugin with parameters
75    WithParams {
76        name: String,
77        #[serde(default)]
78        params: serde_json::Value,
79    },
80}
81
82/// Output formatting options
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(default)]
85pub struct Js2SvgOptions {
86    /// Pretty print output
87    pub pretty: bool,
88
89    /// Indent string
90    pub indent: String,
91
92    /// Use short tags
93    pub use_short_tags: bool,
94
95    /// Final newline
96    pub final_newline: bool,
97
98    /// Line ending style
99    pub eol: LineEnding,
100}
101
102impl Default for Js2SvgOptions {
103    fn default() -> Self {
104        Self {
105            pretty: false,
106            indent: "  ".to_string(),
107            use_short_tags: true,
108            final_newline: true,
109            eol: LineEnding::Lf,
110        }
111    }
112}
113
114/// Line ending options
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub enum LineEnding {
117    /// Unix line endings (\n)
118    #[serde(alias = "lf")]
119    Lf,
120    /// Windows line endings (\r\n)
121    #[serde(alias = "crlf")]
122    Crlf,
123    /// Mac line endings (\r)
124    #[serde(alias = "cr")]
125    Cr,
126}
127
128/// Data URI format options
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub enum DataUriFormat {
131    /// Base64 encoding
132    #[serde(alias = "base64")]
133    Base64,
134    /// URL encoding
135    #[serde(alias = "enc")]
136    Enc,
137    /// Raw text
138    #[serde(alias = "unenc")]
139    Unenc,
140}
141
142#[cfg(feature = "json-schema")]
143use jsonschema::JSONSchema;
144
145use crate::error::VexyError;
146
147impl StreamingConfig {
148    /// Create a memory budget from the config's memory_limit
149    pub fn create_memory_budget(
150        &self,
151    ) -> Option<std::sync::Arc<crate::optimizer::memory::MemoryBudget>> {
152        self.memory_limit
153            .map(|limit| std::sync::Arc::new(crate::optimizer::memory::MemoryBudget::new(limit)))
154    }
155}
156
157impl Config {
158    /// Create a new config with default preset
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// Create a new config with default preset
164    pub fn with_default_preset() -> Self {
165        Self {
166            plugins: vec![
167                PluginConfig::Name("removeDoctype".to_string()),
168                PluginConfig::Name("removeXMLProcInst".to_string()),
169                PluginConfig::Name("removeComments".to_string()),
170                PluginConfig::Name("removeMetadata".to_string()),
171                PluginConfig::Name("removeEditorsNSData".to_string()),
172                // Note: cleanupAttrs not implemented yet
173                PluginConfig::Name("mergeStyles".to_string()),
174                PluginConfig::Name("inlineStyles".to_string()),
175                PluginConfig::Name("minifyStyles".to_string()),
176                PluginConfig::Name("cleanupIds".to_string()),
177                PluginConfig::Name("removeUselessDefs".to_string()),
178                // Note: cleanupNumericValues not implemented yet
179                PluginConfig::Name("convertColors".to_string()),
180                PluginConfig::Name("removeUnknownsAndDefaults".to_string()),
181                PluginConfig::Name("removeNonInheritableGroupAttrs".to_string()),
182                PluginConfig::Name("removeUselessStrokeAndFill".to_string()),
183                // Note: cleanupEnableBackground not implemented yet
184                PluginConfig::Name("removeHiddenElems".to_string()),
185                PluginConfig::Name("removeEmptyText".to_string()),
186                PluginConfig::Name("convertShapeToPath".to_string()),
187                PluginConfig::Name("convertEllipseToCircle".to_string()),
188                PluginConfig::Name("moveElemsAttrsToGroup".to_string()),
189                PluginConfig::Name("moveGroupAttrsToElems".to_string()),
190                PluginConfig::Name("collapseGroups".to_string()),
191                PluginConfig::Name("convertPathData".to_string()),
192                PluginConfig::Name("convertTransform".to_string()),
193                PluginConfig::Name("removeEmptyAttrs".to_string()),
194                PluginConfig::Name("removeEmptyContainers".to_string()),
195                PluginConfig::Name("removeUnusedNS".to_string()),
196                PluginConfig::Name("mergePaths".to_string()),
197                PluginConfig::Name("sortAttrs".to_string()),
198                PluginConfig::Name("sortDefsChildren".to_string()),
199                PluginConfig::Name("removeDesc".to_string()),
200            ],
201            ..Default::default()
202        }
203    }
204
205    /// Load config from a file
206    #[cfg(feature = "json-schema")]
207    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, VexyError> {
208        let content = std::fs::read_to_string(path).map_err(VexyError::Io)?;
209        let config: Value =
210            serde_json::from_str(&content).map_err(|e| VexyError::Config(e.to_string()))?;
211
212        let schema_content = include_str!("../../svgo.schema.json");
213        let schema: Value =
214            serde_json::from_str(schema_content).map_err(|e| VexyError::Config(e.to_string()))?;
215        let compiled_schema =
216            JSONSchema::compile(&schema).map_err(|e| VexyError::Config(e.to_string()))?;
217
218        let result = compiled_schema.validate(&config);
219        if let Err(errors) = result {
220            for error in errors {
221                eprintln!("Configuration error: {}", error);
222            }
223            return Err(VexyError::Config("Invalid configuration".to_string()));
224        }
225
226        serde_json::from_value(config.clone()).map_err(|e| VexyError::Config(e.to_string()))
227    }
228
229    /// Load config from a file (without JSON schema validation)
230    #[cfg(not(feature = "json-schema"))]
231    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, VexyError> {
232        let content = std::fs::read_to_string(path).map_err(VexyError::Io)?;
233        serde_json::from_str(&content).map_err(|e| VexyError::Config(e.to_string()))
234    }
235
236    /// Get a plugin configuration by name
237    pub fn get_plugin(&self, name: &str) -> Option<&PluginConfig> {
238        self.plugins.iter().find(|p| p.name() == name)
239    }
240
241    /// Add a plugin configuration
242    pub fn add_plugin(&mut self, plugin: PluginConfig) {
243        self.plugins.push(plugin);
244    }
245
246    /// Set plugin enabled/disabled status
247    pub fn set_plugin_enabled(&mut self, name: &str, enabled: bool) {
248        for plugin in &mut self.plugins {
249            if plugin.name() == name {
250                match plugin {
251                    PluginConfig::Name(plugin_name) => {
252                        if !enabled {
253                            // Convert to WithParams variant to disable
254                            *plugin = PluginConfig::WithParams {
255                                name: plugin_name.clone(),
256                                params: serde_json::json!({"enabled": false}),
257                            };
258                        }
259                    }
260                    PluginConfig::WithParams { params, .. } => {
261                        if let Some(obj) = params.as_object_mut() {
262                            obj.insert("enabled".to_string(), serde_json::json!(enabled));
263                        } else {
264                            *params = serde_json::json!({"enabled": enabled});
265                        }
266                    }
267                }
268                return;
269            }
270        }
271    }
272
273    /// Configure plugin parameters
274    pub fn configure_plugin(&mut self, name: &str, params: serde_json::Value) {
275        for plugin in &mut self.plugins {
276            if plugin.name() == name {
277                match plugin {
278                    PluginConfig::Name(plugin_name) => {
279                        *plugin = PluginConfig::WithParams {
280                            name: plugin_name.clone(),
281                            params,
282                        };
283                    }
284                    PluginConfig::WithParams {
285                        params: existing_params,
286                        ..
287                    } => {
288                        *existing_params = params;
289                    }
290                }
291                return;
292            }
293        }
294        // If plugin not found, add it with params
295        self.plugins.push(PluginConfig::WithParams {
296            name: name.to_string(),
297            params,
298        });
299    }
300}
301
302impl PluginConfig {
303    /// Create a new plugin config
304    pub fn new(name: String) -> Self {
305        Self::Name(name)
306    }
307
308    /// Get the plugin name
309    pub fn name(&self) -> &str {
310        match self {
311            Self::Name(name) => name,
312            Self::WithParams { name, .. } => name,
313        }
314    }
315
316    /// Get the plugin parameters
317    pub fn params(&self) -> Option<&serde_json::Value> {
318        match self {
319            Self::Name(_) => None,
320            Self::WithParams { params, .. } => Some(params),
321        }
322    }
323
324    /// Get mutable plugin parameters, converting to WithParams variant if needed
325    pub fn params_mut(&mut self) -> &mut serde_json::Value {
326        match self {
327            Self::Name(name) => {
328                let name = name.clone();
329                *self = Self::WithParams {
330                    name,
331                    params: serde_json::json!({}),
332                };
333                match self {
334                    Self::WithParams { params, .. } => params,
335                    _ => unreachable!(),
336                }
337            }
338            Self::WithParams { params, .. } => params,
339        }
340    }
341}
342
343/// Load configuration from a directory (looks for .vexy-vsvgrc, vexy-vsvg.config.json, etc.)
344pub fn load_config_from_directory<P: AsRef<std::path::Path>>(dir: P) -> Result<Config, VexyError> {
345    let dir = dir.as_ref();
346
347    // Try various config file names
348    let config_files = [
349        ".vexy-vsvgrc",
350        ".vexy-vsvgrc.json",
351        "vexy-vsvg.config.json",
352        ".svgorc",
353        ".svgorc.json",
354        "svgo.config.json",
355    ];
356
357    for filename in &config_files {
358        let config_path = dir.join(filename);
359        if config_path.exists() {
360            return Config::from_file(config_path).map_err(|e| VexyError::Config(e.to_string()));
361        }
362    }
363
364    // Return default preset with all standard plugins if no config file found
365    Ok(Config::with_default_preset())
366}