tailwind_rs_core/config/
mod.rs1pub mod build;
7pub mod parser;
8pub mod toml_config;
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(
95 "Build output path cannot be empty".to_string(),
96 ));
97 }
98
99 if self.build.input.is_empty() {
100 return Err(TailwindError::config(
101 "Build input paths cannot be empty".to_string(),
102 ));
103 }
104
105 self.theme.validate()?;
107
108 self.responsive.validate()?;
110
111 Ok(())
112 }
113
114 fn convert_toml_to_json_values(
116 toml_values: HashMap<String, toml::Value>,
117 ) -> HashMap<String, serde_json::Value> {
118 let mut json_values = HashMap::new();
119 for (key, value) in toml_values {
120 match value {
121 toml::Value::String(s) => {
122 json_values.insert(key.clone(), serde_json::Value::String(s));
123 }
124 toml::Value::Integer(i) => {
125 json_values.insert(key.clone(), serde_json::Value::Number(i.into()));
126 }
127 toml::Value::Float(f) => {
128 json_values.insert(
129 key,
130 serde_json::Value::Number(
131 serde_json::Number::from_f64(f).unwrap_or(serde_json::Number::from(0)),
132 ),
133 );
134 }
135 toml::Value::Boolean(b) => {
136 json_values.insert(key, serde_json::Value::Bool(b));
137 }
138 _ => {} }
140 }
141 json_values
142 }
143
144 fn convert_json_to_toml_values(
146 json_values: &HashMap<String, serde_json::Value>,
147 ) -> HashMap<String, toml::Value> {
148 let mut toml_values = HashMap::new();
149 for (key, value) in json_values {
150 match value {
151 serde_json::Value::String(s) => {
152 toml_values.insert(key.clone(), toml::Value::String(s.clone()));
153 }
154 serde_json::Value::Number(n) => {
155 if let Some(i) = n.as_i64() {
156 toml_values.insert(key.clone(), toml::Value::Integer(i));
157 } else if let Some(f) = n.as_f64() {
158 toml_values.insert(key.clone(), toml::Value::Float(f));
159 }
160 }
161 serde_json::Value::Bool(b) => {
162 toml_values.insert(key.clone(), toml::Value::Boolean(*b));
163 }
164 _ => {} }
166 }
167 toml_values
168 }
169
170 fn convert_breakpoints_to_toml(
172 breakpoints: &HashMap<
173 crate::responsive::Breakpoint,
174 crate::responsive::responsive_config::BreakpointConfig,
175 >,
176 ) -> HashMap<String, u32> {
177 let mut toml_breakpoints = HashMap::new();
178 for (breakpoint, config) in breakpoints {
179 toml_breakpoints.insert(breakpoint.to_string().to_lowercase(), config.min_width);
180 }
181 toml_breakpoints
182 }
183}
184
185impl Default for TailwindConfig {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191impl From<TailwindConfigToml> for TailwindConfig {
192 fn from(toml_config: TailwindConfigToml) -> Self {
193 Self {
194 build: toml_config.build.into(),
195 theme: toml_config.theme.into(),
196 responsive: toml_config.responsive.into(),
197 plugins: toml_config.plugins.unwrap_or_default(),
198 custom: Self::convert_toml_to_json_values(toml_config.custom.unwrap_or_default()),
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_config_creation() {
209 let config = TailwindConfig::new();
210 assert!(!config.build.input.is_empty());
211 assert!(!config.build.output.is_empty());
212 }
213
214 #[test]
215 fn test_config_validation() {
216 let mut config = TailwindConfig::new();
217 assert!(config.validate().is_ok());
218
219 config.build.output = "".to_string();
220 assert!(config.validate().is_err());
221 }
222
223 #[test]
224 fn test_toml_parsing() {
225 let toml_content = r#"
226[build]
227input = ["src/**/*.rs"]
228output = "dist/styles.css"
229minify = true
230
231[theme]
232name = "default"
233
234[responsive]
235breakpoints = { sm = 640, md = 768 }
236container_centering = true
237container_padding = 16
238"#;
239
240 let config = TailwindConfig::from_str(toml_content).unwrap();
241 assert_eq!(config.build.output, "dist/styles.css");
242 assert!(config.build.minify);
243 }
244}