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 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 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 responsive = ResponsiveConfig::new();
305 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(), }
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(), },
371 responsive: ResponsiveConfigToml {
372 breakpoints: HashMap::new(),
374 container_centering: false,
375 container_padding: 0,
376 },
377 plugins: config.plugins,
378 custom: HashMap::new(), }
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}