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 ResponsiveConfigToml {
249    pub breakpoints: HashMap<String, u32>,
250    pub container_centering: bool,
251    pub container_padding: u32,
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255struct ThemeToml {
256    pub name: String,
257    pub colors: HashMap<String, String>,
258    pub spacing: HashMap<String, String>,
259    pub border_radius: HashMap<String, String>,
260    pub box_shadows: HashMap<String, String>,
261    pub custom: HashMap<String, toml::Value>,
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 responsive = ResponsiveConfig::new();
305        // TODO: Fix responsive config mapping after refactoring
306        // responsive.breakpoints = toml_config.responsive.breakpoints;
307        // responsive.container_centering = toml_config.responsive.container_centering;
308        // responsive.container_padding =
309        //     crate::responsive::ResponsiveValue::new(toml_config.responsive.container_padding);
310
311        Self {
312            build: BuildConfig {
313                input: toml_config.build.input,
314                output: toml_config.build.output,
315                watch: toml_config.build.watch,
316                minify: toml_config.build.minify,
317                source_maps: toml_config.build.source_maps,
318                purge: toml_config.build.purge,
319                additional_css: toml_config.build.additional_css,
320                postcss_plugins: toml_config.build.postcss_plugins,
321            },
322            theme,
323            responsive,
324            plugins: toml_config.plugins,
325            custom: HashMap::new(), // TODO: Convert TOML values to JSON values
326        }
327    }
328}
329
330impl From<TailwindConfig> for TailwindConfigToml {
331    fn from(config: TailwindConfig) -> Self {
332        let mut theme_colors = HashMap::new();
333        for (name, color) in config.theme.colors {
334            theme_colors.insert(name, color.to_css());
335        }
336
337        let mut theme_spacing = HashMap::new();
338        for (name, spacing) in config.theme.spacing {
339            theme_spacing.insert(name, spacing.to_css());
340        }
341
342        let mut theme_border_radius = HashMap::new();
343        for (name, radius) in config.theme.border_radius {
344            theme_border_radius.insert(name, radius.to_css());
345        }
346
347        let mut theme_box_shadows = HashMap::new();
348        for (name, shadow) in config.theme.box_shadows {
349            theme_box_shadows.insert(name, shadow.to_css());
350        }
351
352        Self {
353            build: BuildConfigToml {
354                input: config.build.input,
355                output: config.build.output,
356                watch: config.build.watch,
357                minify: config.build.minify,
358                source_maps: config.build.source_maps,
359                purge: config.build.purge,
360                additional_css: config.build.additional_css,
361                postcss_plugins: config.build.postcss_plugins,
362            },
363            theme: ThemeToml {
364                name: config.theme.name,
365                colors: theme_colors,
366                spacing: theme_spacing,
367                border_radius: theme_border_radius,
368                box_shadows: theme_box_shadows,
369                custom: HashMap::new(), // TODO: Convert JSON values to TOML values
370            },
371            responsive: ResponsiveConfigToml {
372                // TODO: Fix responsive config mapping after refactoring
373                breakpoints: HashMap::new(),
374                container_centering: false,
375                container_padding: 0,
376            },
377            plugins: config.plugins,
378            custom: HashMap::new(), // TODO: Convert JSON values to TOML values
379        }
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn test_tailwind_config_creation() {
389        let config = TailwindConfig::new();
390        assert_eq!(config.build.input, vec!["src/**/*.rs"]);
391        assert_eq!(config.build.output, "dist/styles.css");
392        assert!(!config.build.watch);
393        assert!(!config.build.minify);
394        assert!(!config.build.source_maps);
395        assert!(config.build.purge);
396    }
397
398    #[test]
399    fn test_build_config_methods() {
400        let mut config = BuildConfig::new();
401
402        config.add_input("examples/**/*.rs");
403        assert!(config.input.contains(&"examples/**/*.rs".to_string()));
404
405        config.set_output("public/css/styles.css");
406        assert_eq!(config.output, "public/css/styles.css");
407
408        config.enable_watch();
409        assert!(config.watch);
410
411        config.enable_minify();
412        assert!(config.minify);
413
414        config.enable_source_maps();
415        assert!(config.source_maps);
416
417        config.disable_purge();
418        assert!(!config.purge);
419    }
420
421    #[test]
422    fn test_tailwind_config_plugins() {
423        let mut config = TailwindConfig::new();
424
425        config.add_plugin("tailwindcss-forms");
426        config.add_plugin("tailwindcss-typography");
427
428        assert_eq!(config.plugins.len(), 2);
429        assert!(config.plugins.contains(&"tailwindcss-forms".to_string()));
430        assert!(
431            config
432                .plugins
433                .contains(&"tailwindcss-typography".to_string())
434        );
435
436        config.remove_plugin("tailwindcss-forms");
437        assert_eq!(config.plugins.len(), 1);
438        assert!(!config.plugins.contains(&"tailwindcss-forms".to_string()));
439        assert!(
440            config
441                .plugins
442                .contains(&"tailwindcss-typography".to_string())
443        );
444    }
445
446    #[test]
447    fn test_tailwind_config_custom() {
448        let mut config = TailwindConfig::new();
449
450        config.set_custom("custom_key", serde_json::json!("custom_value"));
451        assert_eq!(
452            config.get_custom("custom_key"),
453            Some(&serde_json::json!("custom_value"))
454        );
455        assert_eq!(config.get_custom("nonexistent"), None);
456    }
457
458    #[test]
459    fn test_config_from_str_json() {
460        let json_config = r#"{
461            "build": {
462                "input": ["src/**/*.rs"],
463                "output": "dist/styles.css",
464                "watch": false,
465                "minify": false,
466                "source_maps": false,
467                "purge": true,
468                "additional_css": [],
469                "postcss_plugins": []
470            },
471            "theme": {
472                "name": "default",
473                "colors": {},
474                "spacing": {},
475                "border_radius": {},
476                "box_shadows": {},
477                "custom": {}
478            },
479            "responsive": {
480                "breakpoints": {
481                    "Sm": {
482                        "min_width": 640,
483                        "max_width": null,
484                        "enabled": true,
485                        "media_query": null
486                    },
487                    "Md": {
488                        "min_width": 768,
489                        "max_width": null,
490                        "enabled": true,
491                        "media_query": null
492                    },
493                    "Lg": {
494                        "min_width": 1024,
495                        "max_width": null,
496                        "enabled": true,
497                        "media_query": null
498                    },
499                    "Xl": {
500                        "min_width": 1280,
501                        "max_width": null,
502                        "enabled": true,
503                        "media_query": null
504                    },
505                    "Xl2": {
506                        "min_width": 1536,
507                        "max_width": null,
508                        "enabled": true,
509                        "media_query": null
510                    }
511                },
512                "container_centering": true,
513                "container_padding": {
514                    "Base": 16
515                },
516                "defaults": {
517                    "default_breakpoint": "Base",
518                    "include_base": true,
519                    "mobile_first": true
520                }
521            },
522            "plugins": [],
523            "custom": {}
524        }"#;
525
526        let config = TailwindConfig::from_str(json_config).unwrap();
527        assert_eq!(config.build.output, "dist/styles.css");
528        assert_eq!(config.theme.name, "default");
529    }
530
531    #[test]
532    fn test_config_from_str_toml() {
533        let toml_config = r#"plugins = []
534custom = {}
535
536[build]
537input = ["src/**/*.rs"]
538output = "dist/styles.css"
539watch = false
540minify = false
541source_maps = false
542purge = true
543additional_css = []
544postcss_plugins = []
545
546[theme]
547name = "default"
548colors = {}
549spacing = {}
550border_radius = {}
551box_shadows = {}
552custom = {}
553
554[responsive]
555breakpoints = { sm = 640, md = 768, lg = 1024, xl = 1280, "2xl" = 1536 }
556container_centering = true
557container_padding = 16
558"#;
559
560        let config = TailwindConfig::from_str(toml_config).unwrap();
561        assert_eq!(config.build.output, "dist/styles.css");
562        assert_eq!(config.theme.name, "default");
563    }
564}