tailwind_rs_core/
config.rs

1//! Configuration system for tailwind-rs
2
3use crate::error::{Result, TailwindError};
4use crate::responsive::ResponsiveConfig;
5use crate::theme::Theme;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10/// Main configuration for tailwind-rs
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct TailwindConfig {
13    /// Build configuration
14    pub build: BuildConfig,
15    /// Theme configuration
16    pub theme: Theme,
17    /// Responsive configuration
18    pub responsive: ResponsiveConfig,
19    /// Plugin configuration
20    pub plugins: Vec<String>,
21    /// Custom configuration
22    pub custom: HashMap<String, serde_json::Value>,
23}
24
25impl TailwindConfig {
26    /// Create a new configuration with default values
27    pub fn new() -> Self {
28        Self {
29            build: BuildConfig::new(),
30            theme: crate::theme::create_default_theme(),
31            responsive: ResponsiveConfig::new(),
32            plugins: Vec::new(),
33            custom: HashMap::new(),
34        }
35    }
36
37    /// Load configuration from a file
38    pub fn from_file(path: impl Into<PathBuf>) -> Result<Self> {
39        let path = path.into();
40        let content = std::fs::read_to_string(&path).map_err(|e| {
41            TailwindError::config(format!("Failed to read config file {:?}: {}", path, e))
42        })?;
43
44        Self::from_str(&content)
45    }
46
47    /// Load configuration from a string
48    #[allow(clippy::should_implement_trait)]
49    pub fn from_str(content: &str) -> Result<Self> {
50        // Try TOML first, then JSON
51        let trimmed = content.trim();
52        if trimmed.starts_with('[')
53            || trimmed.starts_with('#')
54            || trimmed.starts_with("plugins")
55            || trimmed.starts_with("custom")
56        {
57            // TOML format
58            let config: TailwindConfigToml = toml::from_str(content).map_err(|e| {
59                TailwindError::config(format!("Failed to parse TOML config: {}", e))
60            })?;
61            Ok(config.into())
62        } else {
63            // JSON format
64            serde_json::from_str(content)
65                .map_err(|e| TailwindError::config(format!("Failed to parse JSON config: {}", e)))
66        }
67    }
68
69    /// Save configuration to a file
70    pub fn save_to_file(&self, path: impl Into<PathBuf>) -> Result<()> {
71        let path = path.into();
72        let content = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
73            let toml_config: TailwindConfigToml = self.clone().into();
74            toml::to_string_pretty(&toml_config).map_err(|e| {
75                TailwindError::config(format!("Failed to serialize TOML config: {}", e))
76            })?
77        } else {
78            serde_json::to_string_pretty(self).map_err(|e| {
79                TailwindError::config(format!("Failed to serialize JSON config: {}", e))
80            })?
81        };
82
83        std::fs::write(&path, content).map_err(|e| {
84            TailwindError::config(format!("Failed to write config file {:?}: {}", path, e))
85        })?;
86
87        Ok(())
88    }
89
90    /// Add a plugin
91    pub fn add_plugin(&mut self, plugin: impl Into<String>) {
92        self.plugins.push(plugin.into());
93    }
94
95    /// Remove a plugin
96    pub fn remove_plugin(&mut self, plugin: &str) {
97        self.plugins.retain(|p| p != plugin);
98    }
99
100    /// Set a custom configuration value
101    pub fn set_custom(&mut self, key: impl Into<String>, value: serde_json::Value) {
102        self.custom.insert(key.into(), value);
103    }
104
105    /// Get a custom configuration value
106    pub fn get_custom(&self, key: &str) -> Option<&serde_json::Value> {
107        self.custom.get(key)
108    }
109}
110
111impl Default for TailwindConfig {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117/// Build configuration for tailwind-rs
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct BuildConfig {
120    /// Input paths for source files
121    pub input: Vec<String>,
122    /// Output path for CSS file
123    pub output: String,
124    /// Watch mode for development
125    pub watch: bool,
126    /// Minify output CSS
127    pub minify: bool,
128    /// Source maps generation
129    pub source_maps: bool,
130    /// Purge unused CSS
131    pub purge: bool,
132    /// Additional CSS to include
133    pub additional_css: Vec<String>,
134    /// PostCSS plugins
135    pub postcss_plugins: Vec<String>,
136}
137
138impl BuildConfig {
139    /// Create a new build configuration
140    pub fn new() -> Self {
141        Self {
142            input: vec!["src/**/*.rs".to_string()],
143            output: "dist/styles.css".to_string(),
144            watch: false,
145            minify: false,
146            source_maps: false,
147            purge: true,
148            additional_css: Vec::new(),
149            postcss_plugins: Vec::new(),
150        }
151    }
152
153    /// Add an input path
154    pub fn add_input(&mut self, path: impl Into<String>) {
155        self.input.push(path.into());
156    }
157
158    /// Set the output path
159    pub fn set_output(&mut self, path: impl Into<String>) {
160        self.output = path.into();
161    }
162
163    /// Enable watch mode
164    pub fn enable_watch(&mut self) {
165        self.watch = true;
166    }
167
168    /// Disable watch mode
169    pub fn disable_watch(&mut self) {
170        self.watch = false;
171    }
172
173    /// Enable minification
174    pub fn enable_minify(&mut self) {
175        self.minify = true;
176    }
177
178    /// Disable minification
179    pub fn disable_minify(&mut self) {
180        self.minify = false;
181    }
182
183    /// Enable source maps
184    pub fn enable_source_maps(&mut self) {
185        self.source_maps = true;
186    }
187
188    /// Disable source maps
189    pub fn disable_source_maps(&mut self) {
190        self.source_maps = false;
191    }
192
193    /// Enable CSS purging
194    pub fn enable_purge(&mut self) {
195        self.purge = true;
196    }
197
198    /// Disable CSS purging
199    pub fn disable_purge(&mut self) {
200        self.purge = false;
201    }
202
203    /// Add additional CSS
204    pub fn add_additional_css(&mut self, css: impl Into<String>) {
205        self.additional_css.push(css.into());
206    }
207
208    /// Add a PostCSS plugin
209    pub fn add_postcss_plugin(&mut self, plugin: impl Into<String>) {
210        self.postcss_plugins.push(plugin.into());
211    }
212}
213
214impl Default for BuildConfig {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220/// TOML-specific configuration structure
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222struct TailwindConfigToml {
223    #[serde(rename = "build")]
224    pub build: BuildConfigToml,
225    #[serde(rename = "theme")]
226    pub theme: ThemeToml,
227    #[serde(rename = "responsive")]
228    pub responsive: ResponsiveConfigToml,
229    #[serde(rename = "plugins")]
230    pub plugins: Vec<String>,
231    #[serde(rename = "custom")]
232    pub custom: HashMap<String, toml::Value>,
233}
234
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236struct BuildConfigToml {
237    pub input: Vec<String>,
238    pub output: String,
239    pub watch: bool,
240    pub minify: bool,
241    pub source_maps: bool,
242    pub purge: bool,
243    pub additional_css: Vec<String>,
244    pub postcss_plugins: Vec<String>,
245}
246
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248struct ThemeToml {
249    pub name: String,
250    pub colors: HashMap<String, String>,
251    pub spacing: HashMap<String, String>,
252    pub border_radius: HashMap<String, String>,
253    pub box_shadows: HashMap<String, String>,
254    pub custom: HashMap<String, toml::Value>,
255}
256
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258struct ResponsiveConfigToml {
259    pub breakpoints: HashMap<String, u32>,
260    pub container_centering: bool,
261    pub container_padding: u32,
262}
263
264impl From<TailwindConfigToml> for TailwindConfig {
265    fn from(toml_config: TailwindConfigToml) -> Self {
266        let mut theme = Theme::new(toml_config.theme.name);
267
268        // Convert colors
269        for (name, value) in toml_config.theme.colors {
270            theme.add_color(name, crate::theme::Color::hex(value));
271        }
272
273        // Convert spacing
274        for (name, value) in toml_config.theme.spacing {
275            theme.add_spacing(
276                name,
277                crate::theme::Spacing::rem(value.parse().unwrap_or(1.0)),
278            );
279        }
280
281        // Convert border radius
282        for (name, value) in toml_config.theme.border_radius {
283            theme.add_border_radius(
284                name,
285                crate::theme::BorderRadius::rem(value.parse().unwrap_or(0.0)),
286            );
287        }
288
289        // Convert box shadows
290        for (name, _value) in toml_config.theme.box_shadows {
291            theme.add_box_shadow(
292                name,
293                crate::theme::BoxShadow::new(
294                    0.0,
295                    1.0,
296                    2.0,
297                    0.0,
298                    crate::theme::Color::hex("#000000"),
299                    false,
300                ),
301            );
302        }
303
304        let mut responsive = ResponsiveConfig::new();
305        responsive.breakpoints = toml_config.responsive.breakpoints;
306        responsive.container_centering = toml_config.responsive.container_centering;
307        responsive.container_padding =
308            crate::responsive::ResponsiveValue::new(toml_config.responsive.container_padding);
309
310        Self {
311            build: BuildConfig {
312                input: toml_config.build.input,
313                output: toml_config.build.output,
314                watch: toml_config.build.watch,
315                minify: toml_config.build.minify,
316                source_maps: toml_config.build.source_maps,
317                purge: toml_config.build.purge,
318                additional_css: toml_config.build.additional_css,
319                postcss_plugins: toml_config.build.postcss_plugins,
320            },
321            theme,
322            responsive,
323            plugins: toml_config.plugins,
324            custom: HashMap::new(), // TODO: Convert TOML values to JSON values
325        }
326    }
327}
328
329impl From<TailwindConfig> for TailwindConfigToml {
330    fn from(config: TailwindConfig) -> Self {
331        let mut theme_colors = HashMap::new();
332        for (name, color) in config.theme.colors {
333            theme_colors.insert(name, color.to_css());
334        }
335
336        let mut theme_spacing = HashMap::new();
337        for (name, spacing) in config.theme.spacing {
338            theme_spacing.insert(name, spacing.to_css());
339        }
340
341        let mut theme_border_radius = HashMap::new();
342        for (name, radius) in config.theme.border_radius {
343            theme_border_radius.insert(name, radius.to_css());
344        }
345
346        let mut theme_box_shadows = HashMap::new();
347        for (name, shadow) in config.theme.box_shadows {
348            theme_box_shadows.insert(name, shadow.to_css());
349        }
350
351        Self {
352            build: BuildConfigToml {
353                input: config.build.input,
354                output: config.build.output,
355                watch: config.build.watch,
356                minify: config.build.minify,
357                source_maps: config.build.source_maps,
358                purge: config.build.purge,
359                additional_css: config.build.additional_css,
360                postcss_plugins: config.build.postcss_plugins,
361            },
362            theme: ThemeToml {
363                name: config.theme.name,
364                colors: theme_colors,
365                spacing: theme_spacing,
366                border_radius: theme_border_radius,
367                box_shadows: theme_box_shadows,
368                custom: HashMap::new(), // TODO: Convert JSON values to TOML values
369            },
370            responsive: ResponsiveConfigToml {
371                breakpoints: config.responsive.breakpoints,
372                container_centering: config.responsive.container_centering,
373                container_padding: config.responsive.container_padding.base,
374            },
375            plugins: config.plugins,
376            custom: HashMap::new(), // TODO: Convert JSON values to TOML values
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_tailwind_config_creation() {
387        let config = TailwindConfig::new();
388        assert_eq!(config.build.input, vec!["src/**/*.rs"]);
389        assert_eq!(config.build.output, "dist/styles.css");
390        assert!(!config.build.watch);
391        assert!(!config.build.minify);
392        assert!(!config.build.source_maps);
393        assert!(config.build.purge);
394    }
395
396    #[test]
397    fn test_build_config_methods() {
398        let mut config = BuildConfig::new();
399
400        config.add_input("examples/**/*.rs");
401        assert!(config.input.contains(&"examples/**/*.rs".to_string()));
402
403        config.set_output("public/css/styles.css");
404        assert_eq!(config.output, "public/css/styles.css");
405
406        config.enable_watch();
407        assert!(config.watch);
408
409        config.enable_minify();
410        assert!(config.minify);
411
412        config.enable_source_maps();
413        assert!(config.source_maps);
414
415        config.disable_purge();
416        assert!(!config.purge);
417    }
418
419    #[test]
420    fn test_tailwind_config_plugins() {
421        let mut config = TailwindConfig::new();
422
423        config.add_plugin("tailwindcss-forms");
424        config.add_plugin("tailwindcss-typography");
425
426        assert_eq!(config.plugins.len(), 2);
427        assert!(config.plugins.contains(&"tailwindcss-forms".to_string()));
428        assert!(
429            config
430                .plugins
431                .contains(&"tailwindcss-typography".to_string())
432        );
433
434        config.remove_plugin("tailwindcss-forms");
435        assert_eq!(config.plugins.len(), 1);
436        assert!(!config.plugins.contains(&"tailwindcss-forms".to_string()));
437        assert!(
438            config
439                .plugins
440                .contains(&"tailwindcss-typography".to_string())
441        );
442    }
443
444    #[test]
445    fn test_tailwind_config_custom() {
446        let mut config = TailwindConfig::new();
447
448        config.set_custom("custom_key", serde_json::json!("custom_value"));
449        assert_eq!(
450            config.get_custom("custom_key"),
451            Some(&serde_json::json!("custom_value"))
452        );
453        assert_eq!(config.get_custom("nonexistent"), None);
454    }
455
456    #[test]
457    fn test_config_from_str_json() {
458        let json_config = r#"{
459            "build": {
460                "input": ["src/**/*.rs"],
461                "output": "dist/styles.css",
462                "watch": false,
463                "minify": false,
464                "source_maps": false,
465                "purge": true,
466                "additional_css": [],
467                "postcss_plugins": []
468            },
469            "theme": {
470                "name": "default",
471                "colors": {},
472                "spacing": {},
473                "border_radius": {},
474                "box_shadows": {},
475                "custom": {}
476            },
477            "responsive": {
478                "breakpoints": {
479                    "sm": 640,
480                    "md": 768,
481                    "lg": 1024,
482                    "xl": 1280,
483                    "2xl": 1536
484                },
485                "container_centering": true,
486                "container_padding": {
487                    "base": 16
488                }
489            },
490            "plugins": [],
491            "custom": {}
492        }"#;
493
494        let config = TailwindConfig::from_str(json_config).unwrap();
495        assert_eq!(config.build.output, "dist/styles.css");
496        assert_eq!(config.theme.name, "default");
497    }
498
499    #[test]
500    fn test_config_from_str_toml() {
501        let toml_config = r#"plugins = []
502custom = {}
503
504[build]
505input = ["src/**/*.rs"]
506output = "dist/styles.css"
507watch = false
508minify = false
509source_maps = false
510purge = true
511additional_css = []
512postcss_plugins = []
513
514[theme]
515name = "default"
516colors = {}
517spacing = {}
518border_radius = {}
519box_shadows = {}
520custom = {}
521
522[responsive]
523breakpoints = { sm = 640, md = 768, lg = 1024, xl = 1280, "2xl" = 1536 }
524container_centering = true
525container_padding = 16
526"#;
527
528        let config = TailwindConfig::from_str(toml_config).unwrap();
529        assert_eq!(config.build.output, "dist/styles.css");
530        assert_eq!(config.theme.name, "default");
531    }
532}