aptu_coder_core/languages/
regex_fallback.rs1use crate::parser::SemanticExtractor;
12use crate::types::{FunctionInfo, SemanticAnalysis};
13use regex::Regex;
14use std::sync::LazyLock;
15use tracing;
16
17static CSS_SELECTOR: LazyLock<Regex> = LazyLock::new(|| {
20 #[allow(clippy::expect_used)]
22 Regex::new(r"^[.#][\w-]+[\s,:{]").expect("valid CSS selector pattern")
23});
24
25static YAML_TOP_KEY: LazyLock<Regex> = LazyLock::new(|| {
26 #[allow(clippy::expect_used)]
28 Regex::new(r"^(\w[\w-]*): ").expect("valid YAML top-level key pattern")
29});
30
31static JSON_FIRST_KEY: LazyLock<Regex> = LazyLock::new(|| {
32 #[allow(clippy::expect_used)]
34 Regex::new(r#"^\s{0,2}"(\w+)":"#).expect("valid JSON first-level key pattern")
35});
36
37static TOML_SECTION: LazyLock<Regex> = LazyLock::new(|| {
38 #[allow(clippy::expect_used)]
40 Regex::new(r"^\[([^\]]+)\]").expect("valid TOML section header pattern")
41});
42
43pub fn extract_css(source: &str) -> SemanticAnalysis {
47 let mut functions = Vec::new();
48 for (idx, line) in source.lines().enumerate() {
49 let trimmed = line.trim_start();
50 if CSS_SELECTOR.is_match(trimmed) {
51 let name = trimmed
52 .trim_end_matches(|c: char| c == '{' || c == ',' || c == ':' || c.is_whitespace())
53 .to_string();
54 if !name.is_empty() {
55 let line_no = idx + 1;
56 functions.push(FunctionInfo {
57 name,
58 line: line_no,
59 end_line: line_no,
60 parameters: Vec::new(),
61 return_type: None,
62 });
63 }
64 }
65 }
66 SemanticAnalysis {
67 functions,
68 ..Default::default()
69 }
70}
71
72pub fn extract_yaml(source: &str) -> SemanticAnalysis {
74 let mut functions = Vec::new();
75 for (idx, line) in source.lines().enumerate() {
76 if let Some(caps) = YAML_TOP_KEY.captures(line) {
77 let name = caps[1].to_string();
78 let line_no = idx + 1;
79 functions.push(FunctionInfo {
80 name,
81 line: line_no,
82 end_line: line_no,
83 parameters: Vec::new(),
84 return_type: None,
85 });
86 }
87 }
88 SemanticAnalysis {
89 functions,
90 ..Default::default()
91 }
92}
93
94pub fn extract_json(source: &str) -> SemanticAnalysis {
96 let mut functions = Vec::new();
97 for (idx, line) in source.lines().enumerate() {
98 if let Some(caps) = JSON_FIRST_KEY.captures(line) {
99 let name = caps[1].to_string();
100 let line_no = idx + 1;
101 functions.push(FunctionInfo {
102 name,
103 line: line_no,
104 end_line: line_no,
105 parameters: Vec::new(),
106 return_type: None,
107 });
108 }
109 }
110 SemanticAnalysis {
111 functions,
112 ..Default::default()
113 }
114}
115
116pub fn extract_toml(source: &str) -> SemanticAnalysis {
118 let mut functions = Vec::new();
119 for (idx, line) in source.lines().enumerate() {
120 if let Some(caps) = TOML_SECTION.captures(line) {
121 let name = caps[1].to_string();
122 let line_no = idx + 1;
123 functions.push(FunctionInfo {
124 name,
125 line: line_no,
126 end_line: line_no,
127 parameters: Vec::new(),
128 return_type: None,
129 });
130 }
131 }
132 SemanticAnalysis {
133 functions,
134 ..Default::default()
135 }
136}
137
138#[must_use]
145pub fn extract_astro(source: &str) -> SemanticAnalysis {
146 let block = extract_frontmatter(source);
147 let Some(frontmatter) = block else {
148 return SemanticAnalysis::default();
149 };
150 SemanticExtractor::extract(&frontmatter, "typescript", None, None).unwrap_or_else(|err| {
151 tracing::warn!(error = %err, "astro TypeScript extractor failed; returning empty analysis");
152 SemanticAnalysis::default()
153 })
154}
155
156fn extract_frontmatter(source: &str) -> Option<String> {
157 let mut delimiters = source
158 .lines()
159 .enumerate()
160 .filter(|(_, line)| line.starts_with("---"));
161 let (first, _) = delimiters.next()?;
162 let (second, _) = delimiters.next()?;
163 let block: Vec<&str> = source
164 .lines()
165 .skip(first + 1)
166 .take(second - first - 1)
167 .collect();
168 Some(block.join("\n"))
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_regex_fallback_css_basic() {
177 let source = ".container {\n color: red;\n}\n#header {\n font-size: 16px;\n}\n";
179 let result = extract_css(source);
181 let names: Vec<&str> = result.functions.iter().map(|f| f.name.as_str()).collect();
183 assert!(
184 names.contains(&".container"),
185 "expected .container in {names:?}"
186 );
187 assert!(names.contains(&"#header"), "expected #header in {names:?}");
188 }
189
190 #[test]
191 fn test_regex_fallback_yaml_basic() {
192 let source = "name: my-project\nversion: 1.0\n nested: value\n";
194 let result = extract_yaml(source);
196 let names: Vec<&str> = result.functions.iter().map(|f| f.name.as_str()).collect();
198 assert!(names.contains(&"name"), "expected name in {names:?}");
199 assert!(names.contains(&"version"), "expected version in {names:?}");
200 assert!(
202 !names.contains(&"nested"),
203 "nested must not appear in {names:?}"
204 );
205 }
206
207 #[test]
208 fn test_regex_fallback_json_basic() {
209 let source = "{\n \"name\": \"project\",\n \"version\": \"1.0\"\n}\n";
211 let result = extract_json(source);
213 let names: Vec<&str> = result.functions.iter().map(|f| f.name.as_str()).collect();
215 assert!(names.contains(&"name"), "expected name in {names:?}");
216 assert!(names.contains(&"version"), "expected version in {names:?}");
217 }
218
219 #[test]
220 fn test_regex_fallback_toml_basic() {
221 let source = "[package]\nname = \"my-crate\"\n\n[dependencies]\nregex = \"1\"\n";
223 let result = extract_toml(source);
225 let names: Vec<&str> = result.functions.iter().map(|f| f.name.as_str()).collect();
227 assert!(names.contains(&"package"), "expected package in {names:?}");
228 assert!(
229 names.contains(&"dependencies"),
230 "expected dependencies in {names:?}"
231 );
232 }
233
234 #[test]
235 fn test_regex_fallback_astro_basic() {
236 let source =
238 "---\nimport Foo from './Foo.astro';\nconst title = 'Hello';\n---\n<h1>{title}</h1>\n";
239 let result = extract_astro(source);
241 assert!(
243 !result.imports.is_empty() || !result.functions.is_empty(),
244 "expected imports or functions from frontmatter; got empty result"
245 );
246 }
247
248 #[test]
249 fn test_regex_fallback_astro_no_frontmatter() {
250 let source = "<h1>Hello World</h1>\n<p>No frontmatter here.</p>\n";
252 let result = extract_astro(source);
254 assert!(result.functions.is_empty());
256 assert!(result.imports.is_empty());
257 }
258
259 #[test]
260 fn test_regex_fallback_empty_file() {
261 assert!(extract_css("").functions.is_empty());
263 assert!(extract_yaml("").functions.is_empty());
264 assert!(extract_json("").functions.is_empty());
265 assert!(extract_toml("").functions.is_empty());
266 assert!(extract_astro("").functions.is_empty());
267 }
268}