1use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ScanConfig {
12 pub content_config: ContentConfig,
14 pub parallel_processing: bool,
16 pub max_workers: Option<usize>,
18 pub enable_cache: bool,
20 pub cache_ttl: u64,
22 pub enable_watching: bool,
24 pub watch_debounce: u64,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ContentConfig {
31 pub patterns: Vec<FilePattern>,
33 pub exclude_patterns: Vec<FilePattern>,
35 pub max_file_size: Option<u64>,
37 pub extensions: Vec<String>,
39 pub ignore_dirs: Vec<String>,
41 pub enable_tree_sitter: bool,
43 pub custom_rules: Vec<ExtractionRule>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct FilePattern {
50 pub pattern: String,
52 pub pattern_type: PatternType,
54 pub case_sensitive: bool,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub enum PatternType {
61 Glob,
63 Regex,
65 String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExtractionRule {
72 pub name: String,
74 pub file_pattern: String,
76 pub class_pattern: String,
78 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, enable_watching: false,
91 watch_debounce: 100, }
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), 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 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 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 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 self.matches_glob(&target, &pattern)
183 }
184 PatternType::Regex => {
185 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 fn matches_glob(&self, path: &str, pattern: &str) -> bool {
198 if pattern == "**/*" {
201 return true;
202 }
203
204 if pattern.starts_with("**/") {
205 let suffix = &pattern[3..];
206 if suffix.contains('*') {
207 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 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 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}