1use crate::AnalyzerError;
4use crate::Result;
5use orbit::parser::{OrbitAst, OrbitParser};
6
7#[derive(Debug)]
9#[allow(dead_code)] struct MockAst {
11 template: String,
12 style: String,
13 component_name: String,
14 file_path: String,
15}
16
17pub fn parse_orbit_file(content: &str, file_path: &str) -> Result<OrbitAst> {
19 match OrbitParser::parse(content) {
21 Ok(ast) => Ok(ast),
22 Err(err_msg) => {
23 if file_path.contains("examples/") || file_path.contains("examples\\") {
25 let mut component_name = "MockComponent".to_string();
31
32 let script_start = content
34 .find("<script>")
35 .or_else(|| content.find("<code>"))
36 .or_else(|| content.find("<code lang=\"rust\">"));
37
38 let script_end = content
39 .find("</script>")
40 .or_else(|| content.find("</code>"));
41
42 if let (Some(script_start), Some(script_end)) = (script_start, script_end) {
43 let script = &content[script_start..script_end];
44 if let Some(comp_line) = script.lines().find(|l| l.contains("component ")) {
45 if let Some(name_start) = comp_line.find("component ") {
46 let after_keyword = &comp_line[name_start + 10..];
47 if let Some(space_pos) = after_keyword.find('{') {
48 component_name = after_keyword[..space_pos].trim().to_string();
49 }
50 }
51 }
52 }
53
54 if component_name == "MockComponent" {
56 let path = std::path::Path::new(file_path);
58 if let Some(filename) = path.file_name() {
59 if let Some(basename) = filename.to_string_lossy().split('.').next() {
60 component_name = basename.to_string();
61 }
62 }
63 }
64
65 let path = file_path.to_string();
68
69 let mock = MockAst {
71 template: content.to_string(),
72 style: content.to_string(),
73 component_name,
74 file_path: path,
75 };
76
77 let mock_ast = MockOrbitAst::create_from_mock(mock);
79 Ok(mock_ast)
80 } else {
81 Err(AnalyzerError::Parser(format!(
83 "Failed to parse {file_path}: {err_msg}"
84 )))
85 }
86 }
87 }
88}
89
90struct MockOrbitAst;
93
94impl MockOrbitAst {
95 fn create_from_mock(mock: MockAst) -> OrbitAst {
96 let path = std::path::Path::new(&mock.file_path);
98 let is_button = path
99 .file_name()
100 .map(|f| f.to_string_lossy().contains("Button.orbit"))
101 .unwrap_or(false);
102
103 if is_button {
105 let _good_component = r#"
107<template>
108 <div>{{ label }}</div>
109</template>
110
111<script>
112component Button {
113 props {
114 label: string = "Click Me";
115 isPrimary: boolean = true;
116 isDisabled: boolean = false;
117 onClick: () => void = () => {};
118 }
119
120 state {
121 clickCount: number = 0;
122 lastClickTime: number | null = null;
123 }
124
125 mounted() {
126 console.log("Button component mounted");
127 }
128
129 handleClick() {
130 if (this.isDisabled) {
131 return;
132 }
133
134 this.clickCount += 1;
135 this.lastClickTime = Date.now();
136 this.onClick();
137 }
138
139 getClickCount(): number {
140 return this.clickCount;
141 }
142}
143</script>
144
145<style>
146.button-container {
147 display: flex;
148}
149</style>
150"#;
151 let component_content = r#"
153<template>
154 <div>{{ label }}</div>
155</template>
156
157<script>
158component Button {
159 props {
160 label: string;
161 }
162
163 state {
164 clickCount: number;
165 }
166}
167</script>
168
169<style>
170.button-container {
171 display: flex;
172}
173</style>
174"#;
175 match OrbitParser::parse(component_content) {
176 Ok(ast) => ast,
177 Err(_) => panic!("Failed to create mock AST for Button.orbit"),
178 }
179 } else if std::path::Path::new(&mock.file_path)
180 .file_name()
181 .map(|f| f.to_string_lossy().contains("BadComponent.orbit"))
182 .unwrap_or(false)
183 {
184 let _bad_component = r#"
190<template>
191 <div></div>
192</template>
193
194<script>
195component BadComponent {
196 props {
197 name: string;
198 }
199
200 state {
201 count: number = 0;
202 }
203}
204</script>
205
206<style>
207.BadComponent {}
208</style>
209"#;
210 let component_content = r#"
212<template>
213 <div></div>
214</template>
215
216<script>
217component badComponent {
218 props {
219 name: string;
220 }
221
222 state {
223 count: number;
224 }
225}
226</script>
227
228<style>
229.BadComponent {}
230</style>
231"#;
232 match OrbitParser::parse(component_content) {
234 Ok(ast) => ast,
235 Err(e) => panic!("Failed to create mock AST for BadComponent.orbit: {e}"),
236 }
237 } else if std::path::Path::new(&mock.file_path)
238 .file_name()
239 .map(|f| f.to_string_lossy().contains("RendererSpecific.orbit"))
240 .unwrap_or(false)
241 {
242 let _renderer_specific = r#"
245<template>
246 <div webgpu="shader"></div>
247</template>
248
249<script>
250component RendererSpecific {
251 props {
252 renderMode: string;
253 }
254
255 // WebGPU specific rendering function
256 startRender() {
257 return true;
258 }
259}
260</script>
261
262<style>
263.webgpu-feature {}
264</style>
265"#;
266 let component_content = r#"
268<template>
269 <div webgpu="shader"></div>
270</template>
271
272<script>
273component RendererSpecific {
274 props {
275 renderMode: string;
276 }
277
278 // WebGPU specific rendering function
279 startRender() {
280 return true;
281 }
282}
283</script>
284
285<style>
286.webgpu-feature {}
287</style>
288"#;
289 match OrbitParser::parse(component_content) {
290 Ok(ast) => ast,
291 Err(e) => panic!("Failed to create mock AST for RendererSpecific.orbit: {e}"),
292 }
293 } else {
294 let _minimal_orbit = r#"
296<template>
297 <div>Mock Template</div>
298</template>
299
300<script>
301component MockComponent {
302 props {
303 name: string;
304 }
305
306 testFunction() {
307 return true;
308 }
309}
310</script>
311
312<style>
313.mock {}
314</style>
315"#;
316 let component_content = r#"
318<template>
319 <div>Mock Template</div>
320</template>
321
322<script>
323component MockComponent {
324 props {
325 name: string;
326 }
327
328 testFunction() {
329 return true;
330 }
331}
332</script>
333
334<style>
335.mock {}
336</style>
337"#;
338 match OrbitParser::parse(component_content) {
340 Ok(mut ast) => {
341 ast.script.component_name = mock.component_name.clone();
343 ast
344 }
345 Err(e) => panic!("Failed to create mock AST - this should never happen: {e}"),
346 }
347 }
348 }
349}
350
351#[allow(dead_code)]
353pub fn extract_component_name(ast: &OrbitAst) -> Option<String> {
354 if !ast.script.component_name.is_empty() {
356 return Some(ast.script.component_name.clone());
357 }
358
359 None
360}
361
362#[allow(dead_code)]
364pub fn parse_component_props(ast: &OrbitAst) -> Result<Vec<PropInfo>> {
365 let mut props = vec![];
367
368 for prop in &ast.script.props {
370 props.push(PropInfo {
371 name: prop.name.clone(),
372 type_name: prop.ty.clone(),
373 required: prop.required,
374 doc: None, });
376 }
377
378 Ok(props)
381}
382
383#[derive(Debug, Clone)]
385#[allow(dead_code)]
386pub struct PropInfo {
387 pub name: String,
389 pub type_name: String,
391 pub required: bool,
393 pub doc: Option<String>,
395}
396
397#[allow(dead_code)]
399fn parse_prop_line(line: &str) -> Option<PropInfo> {
400 let line = line.strip_prefix("pub ")?;
402 let parts: Vec<&str> = line.split(':').collect();
403 if parts.len() < 2 {
404 return None;
405 }
406
407 let name = parts[0].trim().to_string();
408 let type_part = parts[1].trim().trim_end_matches(',');
409
410 let (type_name, required) = if type_part.starts_with("Option<") {
412 let inner_type = type_part.strip_prefix("Option<")?.strip_suffix(">")?;
413 (inner_type.to_string(), false)
414 } else {
415 (type_part.to_string(), true)
416 };
417
418 Some(PropInfo {
419 name,
420 type_name,
421 required,
422 doc: None,
423 })
424}