tailwind_rs_scanner/
tree_sitter_parser.rs1use crate::error::{Result, ScannerError};
7
8#[derive(Debug)]
10pub struct TreeSitterParser {
11 languages: Vec<LanguageSupport>,
13}
14
15#[derive(Debug, Clone)]
17pub struct LanguageSupport {
18 pub name: String,
20 pub extensions: Vec<String>,
22 pub supported: bool,
24}
25
26#[derive(Debug, Clone)]
28pub struct ParseResult {
29 pub ast: String,
31 pub errors: Vec<String>,
33 pub language: String,
35}
36
37impl TreeSitterParser {
38 pub fn new() -> Self {
40 let mut languages = Vec::new();
41
42 languages.push(LanguageSupport {
44 name: "rust".to_string(),
45 extensions: vec!["rs".to_string()],
46 supported: true,
47 });
48
49 languages.push(LanguageSupport {
50 name: "javascript".to_string(),
51 extensions: vec!["js".to_string(), "jsx".to_string()],
52 supported: true,
53 });
54
55 languages.push(LanguageSupport {
56 name: "typescript".to_string(),
57 extensions: vec!["ts".to_string(), "tsx".to_string()],
58 supported: true,
59 });
60
61 languages.push(LanguageSupport {
62 name: "html".to_string(),
63 extensions: vec!["html".to_string(), "htm".to_string()],
64 supported: true,
65 });
66
67 Self { languages }
68 }
69
70 pub fn parse(&self, content: &str, language: &str) -> Result<ParseResult> {
72 if !self.is_language_supported(language) {
73 return Err(ScannerError::UnsupportedLanguage(language.to_string()));
74 }
75
76 let errors = Vec::new();
79 let ast = self.generate_ast(content, language);
80
81 Ok(ParseResult {
82 ast,
83 errors,
84 language: language.to_string(),
85 })
86 }
87
88 fn generate_ast(&self, content: &str, language: &str) -> String {
90 match language {
91 "html" => self.parse_html_ast(content),
92 "javascript" | "js" => self.parse_js_ast(content),
93 "typescript" | "ts" => self.parse_ts_ast(content),
94 "rust" => self.parse_rust_ast(content),
95 _ => format!("Basic AST for {}: {}", language, content),
96 }
97 }
98
99 fn parse_html_ast(&self, content: &str) -> String {
101 let mut ast_nodes = Vec::new();
102 let mut in_class_attr = false;
103 let mut current_class = String::new();
104
105 for (i, c) in content.char_indices() {
106 if content[i..].starts_with("class=") {
107 in_class_attr = true;
108 continue;
109 }
110
111 if in_class_attr {
112 if c == '"' || c == '\'' {
113 if !current_class.is_empty() {
114 ast_nodes.push(format!("class: {}", current_class));
115 current_class.clear();
116 }
117 in_class_attr = false;
118 } else if c != ' ' {
119 current_class.push(c);
120 }
121 }
122 }
123
124 if ast_nodes.is_empty() {
125 "HTML AST: No class attributes found".to_string()
126 } else {
127 format!("HTML AST: {}", ast_nodes.join(", "))
128 }
129 }
130
131 fn parse_js_ast(&self, content: &str) -> String {
133 let mut ast_nodes = Vec::new();
134 let mut in_string = false;
135 let mut current_string = String::new();
136 let mut string_delimiter = '"';
137
138 for c in content.chars() {
139 if !in_string && (c == '"' || c == '\'') {
140 in_string = true;
141 string_delimiter = c;
142 current_string.clear();
143 } else if in_string {
144 if c == string_delimiter {
145 if self.looks_like_tailwind_classes(¤t_string) {
147 ast_nodes.push(format!("string: {}", current_string));
148 }
149 in_string = false;
150 } else {
151 current_string.push(c);
152 }
153 }
154 }
155
156 if ast_nodes.is_empty() {
157 "JS AST: No class strings found".to_string()
158 } else {
159 format!("JS AST: {}", ast_nodes.join(", "))
160 }
161 }
162
163 fn parse_ts_ast(&self, content: &str) -> String {
165 self.parse_js_ast(content)
167 }
168
169 fn parse_rust_ast(&self, content: &str) -> String {
171 let mut ast_nodes = Vec::new();
172 let mut in_string = false;
173 let mut current_string = String::new();
174 let mut string_delimiter = '"';
175
176 for c in content.chars() {
177 if !in_string && c == '"' {
178 in_string = true;
179 current_string.clear();
180 } else if in_string {
181 if c == string_delimiter {
182 if self.looks_like_tailwind_classes(¤t_string) {
184 ast_nodes.push(format!("string: {}", current_string));
185 }
186 in_string = false;
187 } else {
188 current_string.push(c);
189 }
190 }
191 }
192
193 if ast_nodes.is_empty() {
194 "Rust AST: No class strings found".to_string()
195 } else {
196 format!("Rust AST: {}", ast_nodes.join(", "))
197 }
198 }
199
200 fn looks_like_tailwind_classes(&self, s: &str) -> bool {
202 s.contains("bg-")
204 || s.contains("text-")
205 || s.contains("p-")
206 || s.contains("m-")
207 || s.contains("w-")
208 || s.contains("h-")
209 || s.contains("flex")
210 || s.contains("grid")
211 || s.contains("hidden")
212 || s.contains("block")
213 }
214
215 pub fn get_supported_languages(&self) -> &[LanguageSupport] {
217 &self.languages
218 }
219
220 pub fn is_language_supported(&self, language: &str) -> bool {
222 self.languages.iter().any(|lang| lang.name == language)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_tree_sitter_parser_creation() {
232 let parser = TreeSitterParser::new();
233 assert!(!parser.languages.is_empty());
234 }
235
236 #[test]
237 fn test_language_support() {
238 let parser = TreeSitterParser::new();
239 assert!(parser.is_language_supported("rust"));
240 assert!(parser.is_language_supported("javascript"));
241 assert!(!parser.is_language_supported("unknown"));
242 }
243
244 #[test]
245 fn test_parse_content() {
246 let parser = TreeSitterParser::new();
247 let content = "let class = 'p-4';";
248 let result = parser.parse(content, "javascript");
249
250 assert!(result.is_ok());
251 let result = result.unwrap();
252 assert_eq!(result.language, "javascript");
253 assert!(result.errors.is_empty());
254 }
255}