Skip to main content

tree_sitter_svelte_next/
lib.rs

1//! Svelte 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_svelte() -> *const ();
7}
8
9/// The tree-sitter [`LanguageFn`] for the Svelte grammar.
10pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_svelte) };
11
12/// The content of the [`node-types.json`][] file for the Svelte grammar.
13pub const NODE_TYPES: &str = include_str!("node-types.json");
14
15/// The syntax highlighting query for the Svelte grammar.
16pub const HIGHLIGHTS_QUERY: &str = include_str!("../queries/highlights.scm");
17
18/// The injections query for Svelte.
19pub const INJECTIONS_QUERY: &str = include_str!("../queries/injections.scm");
20
21/// The local-variable syntax highlighting query for Svelte.
22pub const LOCALS_QUERY: &str = include_str!("../queries/locals.scm");
23
24#[cfg(test)]
25mod tests {
26    #[test]
27    fn test_can_load_grammar() {
28        let mut parser = tree_sitter::Parser::new();
29        parser
30            .set_language(&super::LANGUAGE.into())
31            .expect("Error loading Svelte parser");
32    }
33
34    #[test]
35    fn test_can_parse_svelte() {
36        let mut parser = tree_sitter::Parser::new();
37        parser
38            .set_language(&super::LANGUAGE.into())
39            .expect("Error loading Svelte parser");
40
41        let code = r#"
42<script>
43    let count = $state(0);
44    let doubled = $derived(count * 2);
45
46    function increment() {
47        count++;
48    }
49</script>
50
51{#snippet greeting(name)}
52    <p>Hello {name}!</p>
53{/snippet}
54
55{#each items as item, i (item.id)}
56    <div class="item">{item.name}</div>
57{:else}
58    <p>No items</p>
59{/each}
60
61{#if count > 0}
62    <button onclick={increment}>
63        Clicked {count} {count === 1 ? 'time' : 'times'}
64    </button>
65    {@render greeting('World')}
66{:else}
67    <p>Count is zero</p>
68{/if}
69
70{#await promise}
71    <p>Loading...</p>
72{:then data}
73    <p>Success: {data}</p>
74{:catch error}
75    <p>Error: {error.message}</p>
76{/await}
77
78<style>
79    .item {
80        color: red;
81        font-weight: bold;
82    }
83</style>
84"#;
85
86        let tree = parser.parse(code, None).unwrap();
87        let root = tree.root_node();
88        assert!(
89            !root.has_error(),
90            "Parse tree has errors: {}",
91            root.to_sexp()
92        );
93    }
94
95    #[test]
96    fn test_highlights_query_is_valid() {
97        let language: tree_sitter::Language = super::LANGUAGE.into();
98        tree_sitter::Query::new(&language, super::HIGHLIGHTS_QUERY)
99            .expect("HIGHLIGHTS_QUERY should be a valid query for the Svelte grammar");
100    }
101
102    #[test]
103    fn test_highlights_query_matches_html_nodes() {
104        use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator as _};
105
106        let language: tree_sitter::Language = super::LANGUAGE.into();
107        let mut parser = Parser::new();
108        parser.set_language(&language).unwrap();
109
110        let code = r#"<h1 class="title">Hello</h1>"#;
111        let tree = parser.parse(code, None).unwrap();
112
113        let query = Query::new(&language, super::HIGHLIGHTS_QUERY).unwrap();
114        let mut cursor = QueryCursor::new();
115        let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
116
117        let mut capture_names: Vec<String> = vec![];
118        while let Some(m) = matches.next() {
119            for cap in m.captures {
120                let name = query.capture_names()[cap.index as usize].to_string();
121                if !capture_names.contains(&name) {
122                    capture_names.push(name);
123                }
124            }
125        }
126
127        assert!(
128            capture_names.contains(&"tag".to_string()),
129            "should match tag_name as @tag"
130        );
131        assert!(
132            capture_names.contains(&"attribute".to_string()),
133            "should match attribute_name as @attribute"
134        );
135        assert!(
136            capture_names.contains(&"string".to_string()),
137            "should match attribute_value as @string"
138        );
139    }
140
141    #[test]
142    fn test_injections_query_is_valid() {
143        let language: tree_sitter::Language = super::LANGUAGE.into();
144        tree_sitter::Query::new(&language, super::INJECTIONS_QUERY)
145            .expect("INJECTIONS_QUERY should be a valid query for the Svelte grammar");
146    }
147
148    #[test]
149    fn test_injections_query_matches_script_and_style() {
150        use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator as _};
151
152        let language: tree_sitter::Language = super::LANGUAGE.into();
153        let mut parser = Parser::new();
154        parser.set_language(&language).unwrap();
155
156        let code = r#"
157<script lang="ts">
158    let x: number = 1;
159</script>
160<style>
161    h1 { color: red; }
162</style>
163"#;
164        let tree = parser.parse(code, None).unwrap();
165        let query = Query::new(&language, super::INJECTIONS_QUERY).unwrap();
166        let mut cursor = QueryCursor::new();
167        let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
168
169        let mut matched_languages: Vec<String> = vec![];
170        while let Some(m) = matches.next() {
171            for prop in query.property_settings(m.pattern_index) {
172                if prop.key.as_ref() == "injection.language" {
173                    if let Some(val) = &prop.value {
174                        matched_languages.push(val.to_string());
175                    }
176                }
177            }
178        }
179
180        assert!(
181            matched_languages.contains(&"typescript".to_string()),
182            "should inject TypeScript for script content"
183        );
184        assert!(
185            matched_languages.contains(&"css".to_string()),
186            "should inject CSS for style content"
187        );
188    }
189}