tailwind_rs_core/
utils.rs

1//! Utility functions for tailwind-rs
2
3use crate::error::{Result, TailwindError};
4use std::collections::HashSet;
5
6/// Utility functions for string manipulation
7pub mod string {
8
9    /// Convert a string to kebab-case
10    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    /// Convert a string to camelCase
25    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    /// Convert a string to PascalCase
48    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    /// Check if a string is a valid CSS class name
58    pub fn is_valid_css_class(s: &str) -> bool {
59        if s.is_empty() {
60            return false;
61        }
62
63        // CSS class names can contain letters, digits, hyphens, and underscores
64        // but cannot start with a digit or hyphen
65        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    /// Sanitize a string for use as a CSS class name
75    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                    // Remove invalid characters
84                    '\0'
85                }
86            })
87            .filter(|&c| c != '\0')
88            .collect::<String>()
89            .trim_matches('-')
90            .to_lowercase()
91    }
92}
93
94/// Utility functions for file operations
95pub mod file {
96    use super::*;
97    use std::fs;
98    use std::path::{Path, PathBuf};
99
100    /// Check if a path exists and is a file
101    pub fn exists(path: &Path) -> bool {
102        path.exists() && path.is_file()
103    }
104
105    /// Check if a path exists and is a directory
106    pub fn is_dir(path: &Path) -> bool {
107        path.exists() && path.is_dir()
108    }
109
110    /// Get the file extension from a path
111    pub fn get_extension(path: &Path) -> Option<String> {
112        path.extension()?.to_str().map(|s| s.to_string())
113    }
114
115    /// Get the file name without extension from a path
116    pub fn get_stem(path: &Path) -> Option<String> {
117        path.file_stem()?.to_str().map(|s| s.to_string())
118    }
119
120    /// Get the parent directory of a path
121    pub fn get_parent(path: &Path) -> Option<PathBuf> {
122        path.parent().map(|p| p.to_path_buf())
123    }
124
125    /// Create a directory if it doesn't exist
126    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    /// Read a file to string
136    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    /// Write a string to a file
142    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    /// Get all files matching a glob pattern
152    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
170/// Utility functions for CSS operations
171pub mod css {
172    use super::*;
173
174    /// Parse CSS class names from a string
175    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    /// Join CSS class names into a string
183    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    /// Validate CSS class names
190    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    /// Sanitize CSS class names
203    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    /// Generate CSS custom properties from a map
212    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
228/// Utility functions for validation
229pub mod validation {
230    use super::*;
231
232    /// Validate a file path
233    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    /// Validate a glob pattern
246    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        // Basic validation - check for common issues
252        if pattern.starts_with('/') {
253            return Err(TailwindError::config(
254                "Glob pattern should not start with '/'",
255            ));
256        }
257
258        Ok(())
259    }
260
261    /// Validate a CSS class name
262    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
280/// Utility functions for timing and performance
281pub mod timing {
282    use std::time::{Duration, Instant};
283
284    /// A simple timer for measuring execution time
285    pub struct Timer {
286        start: Instant,
287    }
288
289    impl Timer {
290        /// Create a new timer
291        pub fn new() -> Self {
292            Self {
293                start: Instant::now(),
294            }
295        }
296
297        /// Get the elapsed time
298        pub fn elapsed(&self) -> Duration {
299            self.start.elapsed()
300        }
301
302        /// Get the elapsed time in milliseconds
303        pub fn elapsed_ms(&self) -> u128 {
304            self.elapsed().as_millis()
305        }
306
307        /// Get the elapsed time in microseconds
308        pub fn elapsed_us(&self) -> u128 {
309            self.elapsed().as_micros()
310        }
311
312        /// Reset the timer
313        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        // Note: 123invalid stays as 123invalid, valid-class becomes validclass after sanitization
385    }
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        // Sleep for a short time to test timing
407        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}