Skip to main content

tree_sitter_astro_next/
lib.rs

1//! Astro language support for the [tree-sitter](https://tree-sitter.github.io/) parsing library.
2
3use tree_sitter_language::LanguageFn;
4
5unsafe extern "C" {
6    fn tree_sitter_astro() -> *const ();
7}
8
9/// The tree-sitter [`LanguageFn`] for the Astro grammar.
10pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_astro) };
11
12/// The content of the [`node-types.json`] file for the Astro grammar.
13pub const NODE_TYPES: &str = include_str!("node-types.json");
14
15/// The syntax highlighting query for the Astro grammar.
16pub const HIGHLIGHTS_QUERY: &str = include_str!("../queries/highlights.scm");
17
18/// The injections query for Astro.
19pub 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}