tailwind_rs_core/
utils.rs1use crate::error::{Result, TailwindError};
4use std::collections::HashSet;
5
6pub mod string {
8
9 pub fn to_kebab_case(s: &str) -> String {
11 s.chars()
12 .map(|c| {
13 if c.is_uppercase() {
14 format!("-{}", c.to_lowercase())
15 } else {
16 c.to_string()
17 }
18 })
19 .collect::<String>()
20 .trim_start_matches('-')
21 .to_string()
22 }
23
24 pub fn to_camel_case(s: &str) -> String {
26 let mut result = String::new();
27 let mut capitalize_next = false;
28 let mut first_char = true;
29
30 for c in s.chars() {
31 if c == '-' || c == '_' {
32 capitalize_next = true;
33 } else if capitalize_next {
34 result.push(c.to_uppercase().next().unwrap_or(c));
35 capitalize_next = false;
36 } else if first_char {
37 result.push(c.to_lowercase().next().unwrap_or(c));
38 first_char = false;
39 } else {
40 result.push(c);
41 }
42 }
43
44 result
45 }
46
47 pub fn to_pascal_case(s: &str) -> String {
49 let camel_case = to_camel_case(s);
50 if let Some(first_char) = camel_case.chars().next() {
51 format!("{}{}", first_char.to_uppercase(), &camel_case[1..])
52 } else {
53 camel_case
54 }
55 }
56
57 pub fn is_valid_css_class(s: &str) -> bool {
59 if s.is_empty() {
60 return false;
61 }
62
63 let first_char = s.chars().next().unwrap();
66 if first_char.is_ascii_digit() || first_char == '-' {
67 return false;
68 }
69
70 s.chars()
71 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
72 }
73
74 pub fn sanitize_css_class(s: &str) -> String {
76 s.chars()
77 .map(|c| {
78 if c.is_ascii_alphanumeric() {
79 c
80 } else if c == ' ' || c == '_' {
81 '-'
82 } else {
83 '\0'
85 }
86 })
87 .filter(|&c| c != '\0')
88 .collect::<String>()
89 .trim_matches('-')
90 .to_lowercase()
91 }
92}
93
94pub mod file {
96 use super::*;
97 use std::fs;
98 use std::path::{Path, PathBuf};
99
100 pub fn exists(path: &Path) -> bool {
102 path.exists() && path.is_file()
103 }
104
105 pub fn is_dir(path: &Path) -> bool {
107 path.exists() && path.is_dir()
108 }
109
110 pub fn get_extension(path: &Path) -> Option<String> {
112 path.extension()?.to_str().map(|s| s.to_string())
113 }
114
115 pub fn get_stem(path: &Path) -> Option<String> {
117 path.file_stem()?.to_str().map(|s| s.to_string())
118 }
119
120 pub fn get_parent(path: &Path) -> Option<PathBuf> {
122 path.parent().map(|p| p.to_path_buf())
123 }
124
125 pub fn create_dir_if_not_exists(path: &Path) -> Result<()> {
127 if !path.exists() {
128 fs::create_dir_all(path).map_err(|e| {
129 TailwindError::config(format!("Failed to create directory {:?}: {}", path, e))
130 })?;
131 }
132 Ok(())
133 }
134
135 pub fn read_to_string(path: &Path) -> Result<String> {
137 fs::read_to_string(path)
138 .map_err(|e| TailwindError::config(format!("Failed to read file {:?}: {}", path, e)))
139 }
140
141 pub fn write_string(path: &Path, content: &str) -> Result<()> {
143 if let Some(parent) = path.parent() {
144 create_dir_if_not_exists(parent)?;
145 }
146
147 fs::write(path, content)
148 .map_err(|e| TailwindError::config(format!("Failed to write file {:?}: {}", path, e)))
149 }
150
151 pub fn glob_files(pattern: &str) -> Result<Vec<PathBuf>> {
153 let mut files = Vec::new();
154
155 for entry in glob::glob(pattern).map_err(|e| {
156 TailwindError::config(format!("Invalid glob pattern '{}': {}", pattern, e))
157 })? {
158 let entry = entry
159 .map_err(|e| TailwindError::config(format!("Error reading glob entry: {}", e)))?;
160
161 if entry.is_file() {
162 files.push(entry);
163 }
164 }
165
166 Ok(files)
167 }
168}
169
170pub mod css {
172 use super::*;
173
174 pub fn parse_classes(s: &str) -> HashSet<String> {
176 s.split_whitespace()
177 .map(|s| s.trim().to_string())
178 .filter(|s| !s.is_empty())
179 .collect()
180 }
181
182 pub fn join_classes(classes: &HashSet<String>) -> String {
184 let mut sorted_classes: Vec<String> = classes.iter().cloned().collect();
185 sorted_classes.sort();
186 sorted_classes.join(" ")
187 }
188
189 pub fn validate_classes(classes: &HashSet<String>) -> Result<()> {
191 for class in classes {
192 if !string::is_valid_css_class(class) {
193 return Err(TailwindError::class_generation(format!(
194 "Invalid CSS class name: '{}'",
195 class
196 )));
197 }
198 }
199 Ok(())
200 }
201
202 pub fn sanitize_classes(classes: &HashSet<String>) -> HashSet<String> {
204 classes
205 .iter()
206 .map(|class| string::sanitize_css_class(class))
207 .filter(|class| !class.is_empty())
208 .collect()
209 }
210
211 pub fn generate_custom_properties(
213 properties: &std::collections::HashMap<String, String>,
214 ) -> String {
215 if properties.is_empty() {
216 return String::new();
217 }
218
219 let mut css_properties = Vec::new();
220 for (property, value) in properties {
221 css_properties.push(format!("--{}: {}", property, value));
222 }
223
224 format!("style=\"{}\"", css_properties.join("; "))
225 }
226}
227
228pub mod validation {
230 use super::*;
231
232 pub fn validate_file_path(path: &str) -> Result<()> {
234 if path.is_empty() {
235 return Err(TailwindError::config("File path cannot be empty"));
236 }
237
238 if path.contains("..") {
239 return Err(TailwindError::config("File path cannot contain '..'"));
240 }
241
242 Ok(())
243 }
244
245 pub fn validate_glob_pattern(pattern: &str) -> Result<()> {
247 if pattern.is_empty() {
248 return Err(TailwindError::config("Glob pattern cannot be empty"));
249 }
250
251 if pattern.starts_with('/') {
253 return Err(TailwindError::config(
254 "Glob pattern should not start with '/'",
255 ));
256 }
257
258 Ok(())
259 }
260
261 pub fn validate_css_class(class: &str) -> Result<()> {
263 if class.is_empty() {
264 return Err(TailwindError::class_generation(
265 "CSS class name cannot be empty",
266 ));
267 }
268
269 if !string::is_valid_css_class(class) {
270 return Err(TailwindError::class_generation(format!(
271 "Invalid CSS class name: '{}'",
272 class
273 )));
274 }
275
276 Ok(())
277 }
278}
279
280pub mod timing {
282 use std::time::{Duration, Instant};
283
284 pub struct Timer {
286 start: Instant,
287 }
288
289 impl Timer {
290 pub fn new() -> Self {
292 Self {
293 start: Instant::now(),
294 }
295 }
296
297 pub fn elapsed(&self) -> Duration {
299 self.start.elapsed()
300 }
301
302 pub fn elapsed_ms(&self) -> u128 {
304 self.elapsed().as_millis()
305 }
306
307 pub fn elapsed_us(&self) -> u128 {
309 self.elapsed().as_micros()
310 }
311
312 pub fn reset(&mut self) {
314 self.start = Instant::now();
315 }
316 }
317
318 impl Default for Timer {
319 fn default() -> Self {
320 Self::new()
321 }
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_string_utilities() {
331 assert_eq!(string::to_kebab_case("camelCase"), "camel-case");
332 assert_eq!(string::to_kebab_case("PascalCase"), "pascal-case");
333 assert_eq!(string::to_kebab_case("snake_case"), "snake_case");
334
335 assert_eq!(string::to_camel_case("kebab-case"), "kebabCase");
336 assert_eq!(string::to_camel_case("snake_case"), "snakeCase");
337 assert_eq!(string::to_camel_case("PascalCase"), "pascalCase");
338
339 assert_eq!(string::to_pascal_case("kebab-case"), "KebabCase");
340 assert_eq!(string::to_pascal_case("snake_case"), "SnakeCase");
341 assert_eq!(string::to_pascal_case("camelCase"), "CamelCase");
342
343 assert!(string::is_valid_css_class("valid-class"));
344 assert!(string::is_valid_css_class("valid_class"));
345 assert!(string::is_valid_css_class("validClass"));
346 assert!(!string::is_valid_css_class(""));
347 assert!(!string::is_valid_css_class("123invalid"));
348 assert!(!string::is_valid_css_class("-invalid"));
349
350 assert_eq!(string::sanitize_css_class("valid class"), "valid-class");
351 assert_eq!(string::sanitize_css_class("invalid@class"), "invalidclass");
352 assert_eq!(string::sanitize_css_class(" spaced "), "spaced");
353 }
354
355 #[test]
356 fn test_css_utilities() {
357 let classes_str = "bg-blue-500 text-white hover:bg-blue-600";
358 let classes = css::parse_classes(classes_str);
359
360 assert!(classes.contains("bg-blue-500"));
361 assert!(classes.contains("text-white"));
362 assert!(classes.contains("hover:bg-blue-600"));
363
364 let joined = css::join_classes(&classes);
365 assert!(joined.contains("bg-blue-500"));
366 assert!(joined.contains("text-white"));
367 assert!(joined.contains("hover:bg-blue-600"));
368
369 let valid_classes: HashSet<String> = ["valid-class".to_string(), "valid_class".to_string()]
370 .iter()
371 .cloned()
372 .collect();
373 assert!(css::validate_classes(&valid_classes).is_ok());
374
375 let invalid_classes: HashSet<String> =
376 ["123invalid".to_string(), "valid-class".to_string()]
377 .iter()
378 .cloned()
379 .collect();
380 assert!(css::validate_classes(&invalid_classes).is_err());
381
382 let sanitized = css::sanitize_classes(&invalid_classes);
383 assert!(sanitized.contains("validclass"));
384 }
386
387 #[test]
388 fn test_validation_utilities() {
389 assert!(validation::validate_file_path("valid/path.rs").is_ok());
390 assert!(validation::validate_file_path("").is_err());
391 assert!(validation::validate_file_path("../invalid").is_err());
392
393 assert!(validation::validate_glob_pattern("src/**/*.rs").is_ok());
394 assert!(validation::validate_glob_pattern("").is_err());
395 assert!(validation::validate_glob_pattern("/invalid").is_err());
396
397 assert!(validation::validate_css_class("valid-class").is_ok());
398 assert!(validation::validate_css_class("").is_err());
399 assert!(validation::validate_css_class("123invalid").is_err());
400 }
401
402 #[test]
403 fn test_timing_utilities() {
404 let mut timer = timing::Timer::new();
405
406 std::thread::sleep(std::time::Duration::from_millis(10));
408
409 let elapsed = timer.elapsed();
410 assert!(elapsed.as_millis() >= 10);
411
412 let elapsed_ms = timer.elapsed_ms();
413 assert!(elapsed_ms >= 10);
414
415 let elapsed_us = timer.elapsed_us();
416 assert!(elapsed_us >= 10000);
417
418 timer.reset();
419 let reset_elapsed = timer.elapsed();
420 assert!(reset_elapsed.as_millis() < 10);
421 }
422}