tree_sitter_astro_next/
lib.rs1use tree_sitter_language::LanguageFn;
4
5unsafe extern "C" {
6 fn tree_sitter_astro() -> *const ();
7}
8
9pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_astro) };
11
12pub const NODE_TYPES: &str = include_str!("node-types.json");
14
15pub const HIGHLIGHTS_QUERY: &str = include_str!("../queries/highlights.scm");
17
18pub const INJECTIONS_QUERY: &str = include_str!("../queries/injections.scm");
20
21#[cfg(test)]
22mod tests {
23 #[test]
24 fn test_can_load_grammar() {
25 let mut parser = tree_sitter::Parser::new();
26 parser
27 .set_language(&super::LANGUAGE.into())
28 .expect("Error loading Astro parser");
29 }
30
31 #[test]
32 fn test_can_parse_astro() {
33 let mut parser = tree_sitter::Parser::new();
34 parser
35 .set_language(&super::LANGUAGE.into())
36 .expect("Error loading Astro parser");
37
38 let code = r#"
39---
40import Layout from '../layouts/Layout.astro';
41const title = "Hello";
42const items = [1, 2, 3];
43---
44
45<Layout title={title}>
46 <h1 class="heading">{title}</h1>
47
48 <ul>
49 {items.map((item) => (
50 <li>{item}</li>
51 ))}
52 </ul>
53
54 <script>
55 console.log('Hello from Astro!');
56 </script>
57
58 <style>
59 .heading {
60 color: red;
61 font-weight: bold;
62 }
63 </style>
64</Layout>
65"#;
66
67 let tree = parser.parse(code, None).unwrap();
68 let root = tree.root_node();
69 assert!(
70 !root.has_error(),
71 "Parse tree has errors: {}",
72 root.to_sexp()
73 );
74 }
75
76 #[test]
77 fn test_highlights_query_is_valid() {
78 let language: tree_sitter::Language = super::LANGUAGE.into();
79 tree_sitter::Query::new(&language, super::HIGHLIGHTS_QUERY)
80 .expect("HIGHLIGHTS_QUERY should be a valid query for the Astro grammar");
81 }
82
83 #[test]
84 fn test_highlights_query_matches_html_nodes() {
85 use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator as _};
86
87 let language: tree_sitter::Language = super::LANGUAGE.into();
88 let mut parser = Parser::new();
89 parser.set_language(&language).unwrap();
90
91 let code = r#"<h1 class="title">Hello</h1>"#;
92 let tree = parser.parse(code, None).unwrap();
93
94 let query = Query::new(&language, super::HIGHLIGHTS_QUERY).unwrap();
95 let mut cursor = QueryCursor::new();
96 let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
97
98 let mut capture_names: Vec<String> = vec![];
99 while let Some(m) = matches.next() {
100 for cap in m.captures {
101 let name = query.capture_names()[cap.index as usize].to_string();
102 if !capture_names.contains(&name) {
103 capture_names.push(name);
104 }
105 }
106 }
107
108 assert!(
109 capture_names.contains(&"tag".to_string()),
110 "should match tag_name as @tag"
111 );
112 assert!(
113 capture_names.contains(&"property".to_string()),
114 "should match attribute_name as @property"
115 );
116 assert!(
117 capture_names.contains(&"string".to_string()),
118 "should match attribute_value as @string"
119 );
120 }
121
122 #[test]
123 fn test_injections_query_is_valid() {
124 let language: tree_sitter::Language = super::LANGUAGE.into();
125 tree_sitter::Query::new(&language, super::INJECTIONS_QUERY)
126 .expect("INJECTIONS_QUERY should be a valid query for the Astro grammar");
127 }
128
129 #[test]
130 fn test_injections_query_matches_script_and_style() {
131 use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator as _};
132
133 let language: tree_sitter::Language = super::LANGUAGE.into();
134 let mut parser = Parser::new();
135 parser.set_language(&language).unwrap();
136
137 let code = r#"
138<script>
139 let x: number = 1;
140</script>
141<style>
142 h1 { color: red; }
143</style>
144"#;
145 let tree = parser.parse(code, None).unwrap();
146 let query = Query::new(&language, super::INJECTIONS_QUERY).unwrap();
147 let mut cursor = QueryCursor::new();
148 let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
149
150 let mut matched_languages: Vec<String> = vec![];
151 while let Some(m) = matches.next() {
152 for prop in query.property_settings(m.pattern_index) {
153 if prop.key.as_ref() == "injection.language"
154 && let Some(val) = &prop.value
155 {
156 matched_languages.push(val.to_string());
157 }
158 }
159 }
160
161 assert!(
162 matched_languages.contains(&"typescript".to_string()),
163 "should inject TypeScript for script content"
164 );
165 assert!(
166 matched_languages.contains(&"css".to_string()),
167 "should inject CSS for style content"
168 );
169 }
170
171 #[test]
172 fn test_html_interpolation_structure() {
173 use tree_sitter::Parser;
174
175 let language: tree_sitter::Language = super::LANGUAGE.into();
176 let mut parser = Parser::new();
177 parser.set_language(&language).unwrap();
178
179 let code = r#"{isProduction ? 'Production' : 'Development'}"#;
180 let tree = parser.parse(code, None).unwrap();
181 let root = tree.root_node();
182
183 println!("AST for interpolation: {}", root.to_sexp());
184
185 assert!(!root.has_error(), "Parse tree should not have errors");
186 }
187
188 #[test]
189 fn test_injections_query_matches_html_interpolations() {
190 use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator as _};
191
192 let language: tree_sitter::Language = super::LANGUAGE.into();
193 let mut parser = Parser::new();
194 parser.set_language(&language).unwrap();
195
196 let code = r#"<p>{isProduction ? 'Production' : 'Development'}</p>"#;
197 let tree = parser.parse(code, None).unwrap();
198
199 let query = Query::new(&language, super::INJECTIONS_QUERY).unwrap();
200 let mut cursor = QueryCursor::new();
201 let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
202
203 let mut found_typescript_injection = false;
204 while let Some(m) = matches.next() {
205 for prop in query.property_settings(m.pattern_index) {
206 if prop.key.as_ref() == "injection.language"
207 && let Some(val) = &prop.value
208 && val.as_ref() == "typescript"
209 {
210 found_typescript_injection = true;
211 }
212 }
213 }
214
215 assert!(
216 found_typescript_injection,
217 "should inject TypeScript for HTML interpolation content"
218 );
219 }
220}