1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//! Tree-sitter based Astro source code extractor.
//!
//! Astro components embed TypeScript in a YAML-fenced frontmatter block at the
//! top of the file, delimited by `---` marker lines:
//!
//! ```text
//! ---
//! import Hero from './Hero.astro';
//! interface Props { title: string; }
//! const { title } = Astro.props;
//! ---
//! <html>…</html>
//! ```
//!
//! This extractor locates the frontmatter block, blanks out every line outside
//! it (preserving line numbers), then delegates to [`TypeScriptExtractor`] so
//! all existing TS/JS symbol extraction logic is reused without duplication.
use crate::extraction::typescript_extractor::TypeScriptExtractor;
use crate::extraction::LanguageExtractor;
use crate::types::ExtractionResult;
/// Extracts code graph nodes and edges from Astro component files.
#[derive(Debug)]
pub struct AstroExtractor;
impl AstroExtractor {
/// Extract nodes and edges from an Astro source file.
pub fn extract_astro(file_path: &str, source: &str) -> ExtractionResult {
let masked = Self::mask_non_frontmatter(source);
TypeScriptExtractor::extract_typescript(file_path, &masked)
}
/// Replace every line outside the `---` frontmatter block with an empty line.
///
/// Keeping newlines in place means all line numbers in the AST produced by
/// the TypeScript parser match positions in the original `.astro` file.
fn mask_non_frontmatter(source: &str) -> String {
let lines: Vec<&str> = source.lines().collect();
// Frontmatter requires the very first line to be `---`.
if lines.first().map(|l| l.trim()) != Some("---") {
let blank_lines = lines.len().saturating_sub(1);
return "\n".repeat(blank_lines);
}
// Find the closing `---` marker (first occurrence after line 0).
let content_start = 1;
let content_end = lines[content_start..]
.iter()
.position(|l| l.trim() == "---")
.map_or(lines.len(), |rel| content_start + rel); // unclosed — include everything
lines
.iter()
.enumerate()
.map(|(i, line)| {
if i >= content_start && i < content_end {
*line
} else {
""
}
})
.collect::<Vec<_>>()
.join("\n")
}
}
impl LanguageExtractor for AstroExtractor {
fn extensions(&self) -> &[&str] {
&["astro"]
}
fn language_name(&self) -> &'static str {
"Astro"
}
fn extract(&self, file_path: &str, source: &str) -> ExtractionResult {
Self::extract_astro(file_path, source)
}
}