1use 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct TailwindConfig {
13 pub build: BuildConfig,
15 pub theme: Theme,
17 pub responsive: ResponsiveConfig,
19 pub plugins: Vec<String>,
21 pub custom: HashMap<String, serde_json::Value>,
23}
24
25impl TailwindConfig {
26 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 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 #[allow(clippy::should_implement_trait)]
49 pub fn from_str(content: &str) -> Result<Self> {
50 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 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 serde_json::from_str(content)
65 .map_err(|e| TailwindError::config(format!("Failed to parse JSON config: {}", e)))
66 }
67 }
68
69 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 pub fn add_plugin(&mut self, plugin: impl Into<String>) {
92 self.plugins.push(plugin.into());
93 }
94
95 pub fn remove_plugin(&mut self, plugin: &str) {
97 self.plugins.retain(|p| p != plugin);
98 }
99
100 pub fn set_custom(&mut self, key: impl Into<String>, value: serde_json::Value) {
102 self.custom.insert(key.into(), value);
103 }
104
105 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct BuildConfig {
120 pub input: Vec<String>,
122 pub output: String,
124 pub watch: bool,
126 pub minify: bool,
128 pub source_maps: bool,
130 pub purge: bool,
132 pub additional_css: Vec<String>,
134 pub postcss_plugins: Vec<String>,
136}
137
138impl BuildConfig {
139 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 pub fn add_input(&mut self, path: impl Into<String>) {
155 self.input.push(path.into());
156 }
157
158 pub fn set_output(&mut self, path: impl Into<String>) {
160 self.output = path.into();
161 }
162
163 pub fn enable_watch(&mut self) {
165 self.watch = true;
166 }
167
168 pub fn disable_watch(&mut self) {
170 self.watch = false;
171 }
172
173 pub fn enable_minify(&mut self) {
175 self.minify = true;
176 }
177
178 pub fn disable_minify(&mut self) {
180 self.minify = false;
181 }
182
183 pub fn enable_source_maps(&mut self) {
185 self.source_maps = true;
186 }
187
188 pub fn disable_source_maps(&mut self) {
190 self.source_maps = false;
191 }
192
193 pub fn enable_purge(&mut self) {
195 self.purge = true;
196 }
197
198 pub fn disable_purge(&mut self) {
200 self.purge = false;
201 }
202
203 pub fn add_additional_css(&mut self, css: impl Into<String>) {
205 self.additional_css.push(css.into());
206 }
207
208 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#[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 for (name, value) in toml_config.theme.colors {
270 theme.add_color(name, crate::theme::Color::hex(value));
271 }
272
273 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 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 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(), }
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(), },
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(), }
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}