Skip to main content

svelte_syntax/
lib.rs

1//! Parse Svelte components into typed AST and CST representations.
2//!
3//! This crate provides the syntax layer for working with `.svelte` files in
4//! Rust. It parses components into inspectable tree structures without
5//! compiling them into JavaScript or CSS.
6//!
7//! # Parsing a component
8//!
9//! Use [`parse`] or [`parse_modern_root`] to obtain a typed AST:
10//!
11//! ```
12//! use svelte_syntax::{parse, ParseMode, ParseOptions};
13//!
14//! let doc = parse(
15//!     "<script>let count = 0;</script><button>{count}</button>",
16//!     ParseOptions {
17//!         mode: ParseMode::Modern,
18//!         ..ParseOptions::default()
19//!     },
20//! )?;
21//!
22//! let root = match doc.root {
23//!     svelte_syntax::ast::Root::Modern(root) => root,
24//!     _ => unreachable!(),
25//! };
26//!
27//! // Access the instance script and template fragment.
28//! assert!(root.instance.is_some());
29//! assert!(!root.fragment.nodes.is_empty());
30//! # Ok::<(), svelte_syntax::CompileError>(())
31//! ```
32//!
33//! # Parsing into a CST
34//!
35//! Use [`parse_svelte`] for a tree-sitter concrete syntax tree:
36//!
37//! ```
38//! use svelte_syntax::{SourceId, SourceText, parse_svelte};
39//!
40//! let source = SourceText::new(SourceId::new(0), "<div>hello</div>", None);
41//! let cst = parse_svelte(source)?;
42//!
43//! assert_eq!(cst.root_kind(), "document");
44//! assert!(!cst.has_error());
45//! # Ok::<(), svelte_syntax::CompileError>(())
46//! ```
47//!
48//! # Incremental reparsing
49//!
50//! Both the CST and AST support incremental reparsing. Provide the previous
51//! parse result and a [`CstEdit`] describing the change, and unchanged
52//! subtrees are reused via `Arc` sharing:
53//!
54//! ```
55//! use svelte_syntax::{
56//!     SourceId, SourceText, CstEdit,
57//!     parse_svelte, parse_modern_root, parse_modern_root_incremental,
58//! };
59//!
60//! let before = "<script>let x = 1;</script><div>Hello</div>";
61//! let after  = "<script>let x = 1;</script><div>World</div>";
62//!
63//! let old_root = parse_modern_root(before)?;
64//! let old_cst  = parse_svelte(SourceText::new(SourceId::new(0), before, None))?;
65//!
66//! let edit = CstEdit::replace(before, 37, 42, "World");
67//! let new_root = parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)?;
68//!
69//! // The script was not in the changed range, so it is Arc-reused.
70//! assert!(std::sync::Arc::ptr_eq(
71//!     &old_root.instance.as_ref().unwrap().content,
72//!     &new_root.instance.as_ref().unwrap().content,
73//! ));
74//! # Ok::<(), svelte_syntax::CompileError>(())
75//! ```
76pub mod arena;
77pub mod ast;
78pub mod compat;
79pub mod cst;
80mod error;
81pub mod js;
82mod parse;
83mod primitives;
84mod source;
85
86// --- CST parsing ---
87
88pub use cst::{CstEdit, CstParser, Document, Language, parse_svelte, parse_svelte_incremental};
89
90// --- Errors ---
91
92pub use error::{CompileError, CompilerDiagnosticKind, SourceLocation, SourcePosition};
93
94// --- JavaScript handles ---
95
96pub use js::{ParsedJsExpression, ParsedJsProgram};
97
98// --- AST parsing and element/attribute classification ---
99
100pub use parse::{
101    AttributeKind, ElementKind, ParseMode, ParseOptions, SvelteElementKind,
102    classify_attribute_name, classify_element_name, expression_identifier_name,
103    expression_literal_bool, expression_literal_string, find_matching_brace_close,
104    is_component_name, is_custom_element_name, is_valid_component_name, is_valid_element_name,
105    is_void_element_name, line_column_at_offset, modern_node_end, modern_node_span,
106    modern_node_start, named_children_vec, parse, parse_css, parse_modern_css_nodes,
107    parse_modern_expression_from_text, parse_modern_expression_tag, parse_modern_root,
108    parse_modern_root_incremental, parse_svelte_ignores,
109};
110
111// --- Primitives ---
112
113pub use primitives::{BytePos, SourceId, Span};
114pub use source::SourceText;
115
116#[cfg(test)]
117mod tests {
118    use std::sync::Arc;
119
120    use crate::ast::modern::Node;
121    use crate::cst::{CstEdit, parse_svelte};
122    use crate::parse_modern_root;
123    use crate::primitives::SourceId;
124    use crate::source::SourceText;
125
126    #[test]
127    fn modern_root_scripts_and_template_expressions_keep_oxc_handles() {
128        let root = parse_modern_root("<script>let count = 0;</script><button>{count + 1}</button>")
129            .expect("modern root should parse");
130
131        let instance = root.instance.as_ref().expect("instance script");
132        assert_eq!(instance.oxc_program().body.len(), 1);
133
134        let Node::RegularElement(element) = &root.fragment.nodes[0] else {
135            panic!("expected regular element");
136        };
137        let Node::ExpressionTag(tag) = &element.fragment.nodes[0] else {
138            panic!("expected expression tag");
139        };
140        assert!(tag.expression.parsed().is_some());
141        assert!(tag.expression.oxc_expression().is_some());
142    }
143
144    #[test]
145    fn incremental_parse_reuses_unchanged_script() {
146        let before = "<script>let count = 0;</script>\n<div>Hello</div>";
147        let after = "<script>let count = 0;</script>\n<div>World</div>";
148        let edit_start = "<script>let count = 0;</script>\n<div>".len();
149        let edit_old_end = "<script>let count = 0;</script>\n<div>Hello".len();
150
151        let old_root = parse_modern_root(before).expect("initial parse");
152
153        let before_src = SourceText::new(SourceId::new(1), before, None);
154        let old_cst = parse_svelte(before_src).expect("initial CST parse");
155
156        let edit = CstEdit::replace(before, edit_start, edit_old_end, "World");
157
158        let new_root = crate::parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)
159            .expect("incremental parse");
160
161        // Script was not in any changed range, so it should be Arc-identical.
162        let old_script = old_root.instance.as_ref().expect("old instance");
163        let new_script = new_root.instance.as_ref().expect("new instance");
164        assert!(
165            Arc::ptr_eq(&old_script.content, &new_script.content),
166            "unchanged script should be Arc-reused (same pointer)",
167        );
168
169        // The template fragment should have reparsed.
170        // There may be whitespace text nodes before the element.
171        let el_node = new_root.fragment.nodes.iter().find(|n| matches!(n, Node::RegularElement(_)))
172            .expect("expected regular element in new root fragment");
173        let Node::RegularElement(new_el) = el_node else { unreachable!() };
174        let Node::Text(text) = &new_el.fragment.nodes[0] else {
175            panic!("expected text node in element fragment");
176        };
177        assert_eq!(text.data.as_ref(), "World");
178    }
179
180    /// Verify that tree-sitter's `changed_ranges` reports *structural* changes only.
181    /// A content-only edit (same tree shape) yields empty changed ranges.
182    #[test]
183    fn verify_tree_sitter_changed_ranges_are_structural_only() {
184        // Both produce identical CST structure (element > start_tag > tag_name, text, end_tag > tag_name)
185        let before = "<div>A</div>";
186        let after = "<div>X</div>";
187
188        let before_src = SourceText::new(SourceId::new(50), before, None);
189        let old_cst = parse_svelte(before_src).expect("cst");
190        let mut edited = old_cst.clone_for_incremental();
191        let edit = CstEdit::replace(before, 5, 6, "X");
192        edited.apply_edit(edit);
193
194        let after_src = SourceText::new(SourceId::new(51), after, None);
195        let new_cst = crate::cst::parse_svelte_with_old_tree(after_src, &edited).expect("cst");
196        let ranges = new_cst.changed_ranges(&edited);
197
198        // Tree-sitter does NOT report content-only changes as changed ranges.
199        // Both trees have the same shape: (document (element (start_tag (tag_name)) (text) (end_tag (tag_name))))
200        assert!(
201            ranges.is_empty(),
202            "tree-sitter changed_ranges should be empty for same-structure edit, got: {ranges:?}"
203        );
204
205        // Now test a structural change: adding a new element
206        let before2 = "<div>A</div>";
207        let after2 = "<div>A</div><span>B</span>";
208        let before_src2 = SourceText::new(SourceId::new(52), before2, None);
209        let old_cst2 = parse_svelte(before_src2).expect("cst");
210        let mut edited2 = old_cst2.clone_for_incremental();
211        let edit2 = CstEdit::insert(before2, before2.len(), "<span>B</span>");
212        edited2.apply_edit(edit2);
213
214        let after_src2 = SourceText::new(SourceId::new(53), after2, None);
215        let new_cst2 = crate::cst::parse_svelte_with_old_tree(after_src2, &edited2).expect("cst");
216        let ranges2 = new_cst2.changed_ranges(&edited2);
217
218        // Structural change: new element added. Should have changed ranges.
219        assert!(
220            !ranges2.is_empty(),
221            "tree-sitter changed_ranges should be non-empty for structural edit"
222        );
223    }
224
225    #[test]
226    fn incremental_parse_reuses_unchanged_sibling_element() {
227        let before = "<div>A</div><span>B</span>";
228        let after = "<div>X</div><span>B</span>";
229        let edit_start = "<div>".len();
230        let edit_old_end = "<div>A".len();
231
232        let old_root = parse_modern_root(before).expect("initial parse");
233
234        let before_src = SourceText::new(SourceId::new(3), before, None);
235        let old_cst = parse_svelte(before_src).expect("initial CST parse");
236
237        let edit = CstEdit::replace(before, edit_start, edit_old_end, "X");
238
239        let new_root = crate::parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)
240            .expect("incremental parse");
241
242        // <span>B</span> was unchanged — should be reused.
243        assert_eq!(new_root.fragment.nodes.len(), 2);
244        let Node::RegularElement(new_span) = &new_root.fragment.nodes[1] else {
245            panic!("expected span element");
246        };
247        assert_eq!(new_span.name.as_ref(), "span");
248
249        // Verify the div was reparsed with new content.
250        let Node::RegularElement(new_div) = &new_root.fragment.nodes[0] else {
251            panic!("expected div element");
252        };
253        let Node::Text(text) = &new_div.fragment.nodes[0] else {
254            panic!("expected text in div");
255        };
256        assert_eq!(text.data.as_ref(), "X");
257    }
258}