tailwind_rs_core/config/
mod.rs1pub mod build;
7pub mod toml_config;
8pub mod parser;
9
10pub use build::BuildConfig;
12pub use toml_config::TailwindConfigToml;
13
14use crate::error::{Result, TailwindError};
15use crate::responsive::ResponsiveConfig;
16use crate::theme::Theme;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::path::PathBuf;
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct TailwindConfig {
24 pub build: BuildConfig,
26 pub theme: Theme,
28 pub responsive: ResponsiveConfig,
30 pub plugins: Vec<String>,
32 pub custom: HashMap<String, serde_json::Value>,
34}
35
36impl TailwindConfig {
37 pub fn new() -> Self {
39 Self {
40 build: BuildConfig::new(),
41 theme: crate::theme::create_default_theme(),
42 responsive: ResponsiveConfig::new(),
43 plugins: Vec::new(),
44 custom: HashMap::new(),
45 }
46 }
47
48 pub fn from_file(path: impl Into<PathBuf>) -> Result<Self> {
50 let path = path.into();
51 let content = std::fs::read_to_string(&path).map_err(|e| {
52 TailwindError::config(format!("Failed to read config file {:?}: {}", path, e))
53 })?;
54
55 Self::from_str(&content)
56 }
57
58 pub fn from_str(content: &str) -> Result<Self> {
60 if content.trim().starts_with('[') || content.trim().starts_with('#') {
62 let toml_config: TailwindConfigToml = toml::from_str(content)
63 .map_err(|e| TailwindError::config(format!("TOML parsing error: {}", e)))?;
64 Ok(toml_config.into())
65 } else {
66 serde_json::from_str(content)
67 .map_err(|e| TailwindError::config(format!("JSON parsing error: {}", e)))
68 }
69 }
70
71 pub fn save_to_file(&self, path: impl Into<PathBuf>) -> Result<()> {
73 let path = path.into();
74 let content = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
75 let toml_config: TailwindConfigToml = self.clone().into();
76 toml::to_string_pretty(&toml_config)
77 .map_err(|e| TailwindError::config(format!("TOML serialization error: {}", e)))?
78 } else {
79 serde_json::to_string_pretty(self)
80 .map_err(|e| TailwindError::config(format!("JSON serialization error: {}", e)))?
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 validate(&self) -> Result<()> {
92 if self.build.output.is_empty() {
94 return Err(TailwindError::config("Build output path cannot be empty".to_string()));
95 }
96
97 if self.build.input.is_empty() {
98 return Err(TailwindError::config("Build input paths cannot be empty".to_string()));
99 }
100
101 self.theme.validate()?;
103
104 self.responsive.validate()?;
106
107 Ok(())
108 }
109
110 fn convert_toml_to_json_values(toml_values: HashMap<String, toml::Value>) -> HashMap<String, serde_json::Value> {
112 let mut json_values = HashMap::new();
113 for (key, value) in toml_values {
114 match value {
115 toml::Value::String(s) => { json_values.insert(key.clone(), serde_json::Value::String(s)); }
116 toml::Value::Integer(i) => { json_values.insert(key.clone(), serde_json::Value::Number(i.into())); }
117 toml::Value::Float(f) => { json_values.insert(key, serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap_or(serde_json::Number::from(0)))); }
118 toml::Value::Boolean(b) => { json_values.insert(key, serde_json::Value::Bool(b)); }
119 _ => {} }
121 }
122 json_values
123 }
124
125 fn convert_json_to_toml_values(json_values: &HashMap<String, serde_json::Value>) -> HashMap<String, toml::Value> {
127 let mut toml_values = HashMap::new();
128 for (key, value) in json_values {
129 match value {
130 serde_json::Value::String(s) => { toml_values.insert(key.clone(), toml::Value::String(s.clone())); }
131 serde_json::Value::Number(n) => {
132 if let Some(i) = n.as_i64() {
133 toml_values.insert(key.clone(), toml::Value::Integer(i));
134 } else if let Some(f) = n.as_f64() {
135 toml_values.insert(key.clone(), toml::Value::Float(f));
136 }
137 }
138 serde_json::Value::Bool(b) => { toml_values.insert(key.clone(), toml::Value::Boolean(*b)); }
139 _ => {} }
141 }
142 toml_values
143 }
144
145 fn convert_breakpoints_to_toml(breakpoints: &HashMap<crate::responsive::Breakpoint, crate::responsive::responsive_config::BreakpointConfig>) -> HashMap<String, u32> {
147 let mut toml_breakpoints = HashMap::new();
148 for (breakpoint, config) in breakpoints {
149 toml_breakpoints.insert(breakpoint.to_string().to_lowercase(), config.min_width);
150 }
151 toml_breakpoints
152 }
153}
154
155impl Default for TailwindConfig {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161impl From<TailwindConfigToml> for TailwindConfig {
162 fn from(toml_config: TailwindConfigToml) -> Self {
163 Self {
164 build: toml_config.build.into(),
165 theme: toml_config.theme.into(),
166 responsive: toml_config.responsive.into(),
167 plugins: toml_config.plugins.unwrap_or_default(),
168 custom: Self::convert_toml_to_json_values(toml_config.custom.unwrap_or_default()),
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_config_creation() {
179 let config = TailwindConfig::new();
180 assert!(!config.build.input.is_empty());
181 assert!(!config.build.output.is_empty());
182 }
183
184 #[test]
185 fn test_config_validation() {
186 let mut config = TailwindConfig::new();
187 assert!(config.validate().is_ok());
188
189 config.build.output = "".to_string();
190 assert!(config.validate().is_err());
191 }
192
193 #[test]
194 fn test_toml_parsing() {
195 let toml_content = r#"
196[build]
197input = ["src/**/*.rs"]
198output = "dist/styles.css"
199minify = true
200
201[theme]
202name = "default"
203
204[responsive]
205breakpoints = { sm = 640, md = 768 }
206container_centering = true
207container_padding = 16
208"#;
209
210 let config = TailwindConfig::from_str(toml_content).unwrap();
211 assert_eq!(config.build.output, "dist/styles.css");
212 assert!(config.build.minify);
213 }
214}