Skip to main content

tailwind_rs_scanner/
content_config.rs

1//! Content configuration for scanning
2//!
3//! This module provides configuration structures for content scanning
4//! and file pattern matching.
5
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9/// Main scan configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ScanConfig {
12    /// Content configuration
13    pub content_config: ContentConfig,
14    /// Enable parallel processing
15    pub parallel_processing: bool,
16    /// Maximum number of parallel workers
17    pub max_workers: Option<usize>,
18    /// Enable caching
19    pub enable_cache: bool,
20    /// Cache TTL in seconds
21    pub cache_ttl: u64,
22    /// Enable file watching
23    pub enable_watching: bool,
24    /// Watch debounce time in milliseconds
25    pub watch_debounce: u64,
26}
27
28/// Content configuration
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ContentConfig {
31    /// File patterns to include
32    pub patterns: Vec<FilePattern>,
33    /// File patterns to exclude
34    pub exclude_patterns: Vec<FilePattern>,
35    /// Maximum file size to scan (in bytes)
36    pub max_file_size: Option<u64>,
37    /// File extensions to scan
38    pub extensions: Vec<String>,
39    /// Directories to ignore
40    pub ignore_dirs: Vec<String>,
41    /// Enable tree-sitter parsing
42    pub enable_tree_sitter: bool,
43    /// Custom class extraction rules
44    pub custom_rules: Vec<ExtractionRule>,
45}
46
47/// File pattern for matching files
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct FilePattern {
50    /// Pattern string (glob pattern)
51    pub pattern: String,
52    /// Pattern type
53    pub pattern_type: PatternType,
54    /// Whether pattern is case sensitive
55    pub case_sensitive: bool,
56}
57
58/// Pattern matching type
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub enum PatternType {
61    /// Glob pattern
62    Glob,
63    /// Regex pattern
64    Regex,
65    /// Simple string match
66    String,
67}
68
69/// Custom extraction rule
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExtractionRule {
72    /// Rule name
73    pub name: String,
74    /// File pattern this rule applies to
75    pub file_pattern: String,
76    /// Regex pattern for class extraction
77    pub class_pattern: String,
78    /// Context extraction pattern
79    pub context_pattern: Option<String>,
80}
81
82impl Default for ScanConfig {
83    fn default() -> Self {
84        Self {
85            content_config: ContentConfig::default(),
86            parallel_processing: true,
87            max_workers: None,
88            enable_cache: true,
89            cache_ttl: 3600, // 1 hour
90            enable_watching: false,
91            watch_debounce: 100, // 100ms
92        }
93    }
94}
95
96impl Default for ContentConfig {
97    fn default() -> Self {
98        Self {
99            patterns: vec![
100                FilePattern::new("**/*.rs", PatternType::Glob),
101                FilePattern::new("**/*.js", PatternType::Glob),
102                FilePattern::new("**/*.ts", PatternType::Glob),
103                FilePattern::new("**/*.jsx", PatternType::Glob),
104                FilePattern::new("**/*.tsx", PatternType::Glob),
105                FilePattern::new("**/*.html", PatternType::Glob),
106                FilePattern::new("**/*.vue", PatternType::Glob),
107                FilePattern::new("**/*.svelte", PatternType::Glob),
108            ],
109            exclude_patterns: vec![
110                FilePattern::new("**/node_modules/**", PatternType::Glob),
111                FilePattern::new("**/target/**", PatternType::Glob),
112                FilePattern::new("**/.git/**", PatternType::Glob),
113                FilePattern::new("**/dist/**", PatternType::Glob),
114                FilePattern::new("**/build/**", PatternType::Glob),
115            ],
116            max_file_size: Some(10 * 1024 * 1024), // 10MB
117            extensions: vec![
118                "rs".to_string(),
119                "js".to_string(),
120                "ts".to_string(),
121                "jsx".to_string(),
122                "tsx".to_string(),
123                "html".to_string(),
124                "vue".to_string(),
125                "svelte".to_string(),
126                "css".to_string(),
127                "scss".to_string(),
128                "less".to_string(),
129            ],
130            ignore_dirs: vec![
131                "node_modules".to_string(),
132                "target".to_string(),
133                ".git".to_string(),
134                "dist".to_string(),
135                "build".to_string(),
136                ".next".to_string(),
137                ".nuxt".to_string(),
138            ],
139            enable_tree_sitter: true,
140            custom_rules: Vec::new(),
141        }
142    }
143}
144
145impl FilePattern {
146    /// Create a new file pattern
147    pub fn new(pattern: &str, pattern_type: PatternType) -> Self {
148        Self {
149            pattern: pattern.to_string(),
150            pattern_type,
151            case_sensitive: false,
152        }
153    }
154
155    /// Create a case-sensitive file pattern
156    pub fn new_case_sensitive(pattern: &str, pattern_type: PatternType) -> Self {
157        Self {
158            pattern: pattern.to_string(),
159            pattern_type,
160            case_sensitive: true,
161        }
162    }
163
164    /// Check if pattern matches a file path
165    pub fn matches_file(&self, file_path: &Path) -> bool {
166        let path_str = file_path.to_string_lossy();
167        let target = if self.case_sensitive {
168            path_str.to_string()
169        } else {
170            path_str.to_lowercase()
171        };
172
173        let pattern = if self.case_sensitive {
174            self.pattern.clone()
175        } else {
176            self.pattern.to_lowercase()
177        };
178
179        match self.pattern_type {
180            PatternType::Glob => {
181                // Simple glob matching (in a real implementation, use a proper glob library)
182                self.matches_glob(&target, &pattern)
183            }
184            PatternType::Regex => {
185                // Regex matching
186                if let Ok(regex) = regex::Regex::new(&pattern) {
187                    regex.is_match(&target)
188                } else {
189                    false
190                }
191            }
192            PatternType::String => target.contains(&pattern),
193        }
194    }
195
196    /// Simple glob matching implementation
197    fn matches_glob(&self, path: &str, pattern: &str) -> bool {
198        // This is a simplified glob matcher
199        // In a real implementation, use a proper glob library like globset
200        if pattern == "**/*" {
201            return true;
202        }
203
204        if pattern.starts_with("**/") {
205            let suffix = &pattern[3..];
206            if suffix.contains('*') {
207                // Handle patterns like **/*.rs
208                let suffix_parts: Vec<&str> = suffix.split('*').collect();
209                if suffix_parts.len() == 2 {
210                    let prefix = suffix_parts[0];
211                    let suffix_end = suffix_parts[1];
212                    return path.ends_with(suffix_end);
213                }
214            }
215            return path.ends_with(suffix);
216        }
217
218        if pattern.contains('*') {
219            let parts: Vec<&str> = pattern.split('*').collect();
220            if parts.len() == 2 {
221                let prefix = parts[0];
222                let suffix = parts[1];
223                return path.starts_with(prefix) && path.ends_with(suffix);
224            }
225        }
226
227        path == pattern
228    }
229}
230
231impl ExtractionRule {
232    /// Create a new extraction rule
233    pub fn new(name: &str, file_pattern: &str, class_pattern: &str) -> Self {
234        Self {
235            name: name.to_string(),
236            file_pattern: file_pattern.to_string(),
237            class_pattern: class_pattern.to_string(),
238            context_pattern: None,
239        }
240    }
241
242    /// Create an extraction rule with context
243    pub fn new_with_context(
244        name: &str,
245        file_pattern: &str,
246        class_pattern: &str,
247        context_pattern: &str,
248    ) -> Self {
249        Self {
250            name: name.to_string(),
251            file_pattern: file_pattern.to_string(),
252            class_pattern: class_pattern.to_string(),
253            context_pattern: Some(context_pattern.to_string()),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use std::path::Path;
262
263    #[test]
264    fn test_scan_config_default() {
265        let config = ScanConfig::default();
266        assert!(config.parallel_processing);
267        assert!(config.enable_cache);
268        assert!(!config.enable_watching);
269    }
270
271    #[test]
272    fn test_content_config_default() {
273        let config = ContentConfig::default();
274        assert!(!config.patterns.is_empty());
275        assert!(!config.exclude_patterns.is_empty());
276        assert!(config.enable_tree_sitter);
277    }
278
279    #[test]
280    fn test_file_pattern_creation() {
281        let pattern = FilePattern::new("**/*.rs", PatternType::Glob);
282        assert_eq!(pattern.pattern, "**/*.rs");
283        assert!(!pattern.case_sensitive);
284    }
285
286    #[test]
287    fn test_file_pattern_matching() {
288        let pattern = FilePattern::new("**/*.rs", PatternType::Glob);
289        let rust_file = Path::new("src/main.rs");
290        let js_file = Path::new("src/main.js");
291
292        assert!(pattern.matches_file(rust_file));
293        assert!(!pattern.matches_file(js_file));
294    }
295
296    #[test]
297    fn test_extraction_rule_creation() {
298        let rule = ExtractionRule::new("rust_classes", "**/*.rs", r#"class\s*=\s*"([^"]+)""#);
299
300        assert_eq!(rule.name, "rust_classes");
301        assert_eq!(rule.file_pattern, "**/*.rs");
302        assert_eq!(rule.class_pattern, r#"class\s*=\s*"([^"]+)""#);
303        assert!(rule.context_pattern.is_none());
304    }
305
306    #[test]
307    fn test_extraction_rule_with_context() {
308        let rule = ExtractionRule::new_with_context(
309            "html_classes",
310            "**/*.html",
311            r#"class\s*=\s*"([^"]+)""#,
312            r#"<(\w+)\s+[^>]*class\s*=\s*"[^"]*""#,
313        );
314
315        assert_eq!(rule.name, "html_classes");
316        assert!(rule.context_pattern.is_some());
317    }
318}