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 automatically:
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(crate) mod estree;
82pub mod js;
83mod parse;
84mod primitives;
85mod source;
86
87// --- CST parsing ---
88
89pub use cst::{
90    CstEdit, CstParser, Document, ExpressionCache, Language, ParsedDocument,
91    parse_svelte, parse_svelte_incremental,
92    // Wrapper types
93    Root, Element, TextNode, CommentNode, IfBlock, EachBlock, AwaitBlock,
94    KeyBlock, SnippetBlock, ExpressionTag, HtmlTag, ConstTag, DebugTag,
95    RenderTag, AttachTag, AttributeNode, AttributeValuePart, StartTag,
96    ElseClause, Alternate, TemplateNode, ChildIter, AttributeIter, classify_node,
97};
98
99// --- Errors ---
100
101pub use error::{CompileError, DiagnosticKind, LineColumn, SourcePosition};
102
103// --- JavaScript handles ---
104
105pub use js::{JsExpression, JsProgram};
106
107// --- AST parsing and element/attribute classification ---
108
109pub use parse::{
110    AttributeKind, ElementKind, ParseMode, ParseOptions, ParseCounters, ParseTimings,
111    SvelteElementKind, classify_attribute_name, classify_element_name,
112    find_matching_brace_close, is_component_name, is_custom_element_name,
113    is_valid_component_name, is_valid_element_name, is_void_element_name,
114    legacy_root_from_modern, line_column_at_offset, parse, parse_css,
115    parse_legacy_root_from_cst, parse_modern_css_nodes, parse_modern_expression_from_text,
116    parse_modern_expression_tag, parse_modern_root, parse_modern_root_incremental,
117    parse_modern_root_timed, parse_svelte_ignores,
118    read_parse_counters, reset_parse_counters,
119};
120
121// --- Primitives ---
122
123pub use primitives::{BytePos, SourceId, Span};
124pub use source::SourceText;
125
126#[cfg(test)]
127mod tests {
128    use std::sync::Arc;
129
130    use crate::ast::modern::Node;
131    use crate::cst::{CstEdit, parse_svelte};
132    use crate::parse_modern_root;
133    use crate::primitives::SourceId;
134    use crate::source::SourceText;
135
136    #[test]
137    fn modern_root_scripts_and_template_expressions_keep_oxc_handles() {
138        let root = parse_modern_root("<script>let count = 0;</script><button>{count + 1}</button>")
139            .expect("modern root should parse");
140
141        let instance = root.instance.as_ref().expect("instance script");
142        assert_eq!(instance.oxc_program().body.len(), 1);
143
144        let Node::RegularElement(element) = &root.fragment.nodes[0] else {
145            panic!("expected regular element");
146        };
147        let Node::ExpressionTag(tag) = &element.fragment.nodes[0] else {
148            panic!("expected expression tag");
149        };
150        assert!(tag.expression.parsed().is_some());
151        assert!(tag.expression.oxc_expression().is_some());
152    }
153
154    #[test]
155    fn incremental_parse_reuses_unchanged_script() {
156        let before = "<script>let count = 0;</script>\n<div>Hello</div>";
157        let after = "<script>let count = 0;</script>\n<div>World</div>";
158        let edit_start = "<script>let count = 0;</script>\n<div>".len();
159        let edit_old_end = "<script>let count = 0;</script>\n<div>Hello".len();
160
161        let old_root = parse_modern_root(before).expect("initial parse");
162
163        let before_src = SourceText::new(SourceId::new(1), before, None);
164        let old_cst = parse_svelte(before_src).expect("initial CST parse");
165
166        let edit = CstEdit::replace(before, edit_start, edit_old_end, "World");
167
168        let new_root = crate::parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)
169            .expect("incremental parse");
170
171        // Script was not in any changed range, so it should be Arc-identical.
172        let old_script = old_root.instance.as_ref().expect("old instance");
173        let new_script = new_root.instance.as_ref().expect("new instance");
174        assert!(
175            Arc::ptr_eq(&old_script.content, &new_script.content),
176            "unchanged script should be Arc-reused (same pointer)",
177        );
178
179        // The template fragment should have reparsed.
180        // There may be whitespace text nodes before the element.
181        let el_node = new_root.fragment.nodes.iter().find(|n| matches!(n, Node::RegularElement(_)))
182            .expect("expected regular element in new root fragment");
183        let Node::RegularElement(new_el) = el_node else { unreachable!() };
184        let Node::Text(text) = &new_el.fragment.nodes[0] else {
185            panic!("expected text node in element fragment");
186        };
187        assert_eq!(text.data.as_ref(), "World");
188    }
189
190    /// Verify that tree-sitter's `changed_ranges` reports *structural* changes only.
191    /// A content-only edit (same tree shape) yields empty changed ranges.
192    #[test]
193    fn verify_tree_sitter_changed_ranges_are_structural_only() {
194        // Both produce identical CST structure (element > start_tag > tag_name, text, end_tag > tag_name)
195        let before = "<div>A</div>";
196        let after = "<div>X</div>";
197
198        let before_src = SourceText::new(SourceId::new(50), before, None);
199        let old_cst = parse_svelte(before_src).expect("cst");
200        let mut edited = old_cst.clone_for_incremental();
201        let edit = CstEdit::replace(before, 5, 6, "X");
202        edited.apply_edit(edit);
203
204        let after_src = SourceText::new(SourceId::new(51), after, None);
205        let new_cst = crate::cst::parse_svelte_with_old_tree(after_src, &edited).expect("cst");
206        let ranges = new_cst.changed_ranges(&edited);
207
208        // Tree-sitter does NOT report content-only changes as changed ranges.
209        // Both trees have the same shape: (document (element (start_tag (tag_name)) (text) (end_tag (tag_name))))
210        assert!(
211            ranges.is_empty(),
212            "tree-sitter changed_ranges should be empty for same-structure edit, got: {ranges:?}"
213        );
214
215        // Now test a structural change: adding a new element
216        let before2 = "<div>A</div>";
217        let after2 = "<div>A</div><span>B</span>";
218        let before_src2 = SourceText::new(SourceId::new(52), before2, None);
219        let old_cst2 = parse_svelte(before_src2).expect("cst");
220        let mut edited2 = old_cst2.clone_for_incremental();
221        let edit2 = CstEdit::insert(before2, before2.len(), "<span>B</span>");
222        edited2.apply_edit(edit2);
223
224        let after_src2 = SourceText::new(SourceId::new(53), after2, None);
225        let new_cst2 = crate::cst::parse_svelte_with_old_tree(after_src2, &edited2).expect("cst");
226        let ranges2 = new_cst2.changed_ranges(&edited2);
227
228        // Structural change: new element added. Should have changed ranges.
229        assert!(
230            !ranges2.is_empty(),
231            "tree-sitter changed_ranges should be non-empty for structural edit"
232        );
233    }
234
235    #[test]
236    fn incremental_parse_reuses_unchanged_sibling_element() {
237        let before = "<div>A</div><span>B</span>";
238        let after = "<div>X</div><span>B</span>";
239        let edit_start = "<div>".len();
240        let edit_old_end = "<div>A".len();
241
242        let old_root = parse_modern_root(before).expect("initial parse");
243
244        let before_src = SourceText::new(SourceId::new(3), before, None);
245        let old_cst = parse_svelte(before_src).expect("initial CST parse");
246
247        let edit = CstEdit::replace(before, edit_start, edit_old_end, "X");
248
249        let new_root = crate::parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)
250            .expect("incremental parse");
251
252        // <span>B</span> was unchanged — should be reused.
253        assert_eq!(new_root.fragment.nodes.len(), 2);
254        let Node::RegularElement(new_span) = &new_root.fragment.nodes[1] else {
255            panic!("expected span element");
256        };
257        assert_eq!(new_span.name.as_ref(), "span");
258
259        // Verify the div was reparsed with new content.
260        let Node::RegularElement(new_div) = &new_root.fragment.nodes[0] else {
261            panic!("expected div element");
262        };
263        let Node::Text(text) = &new_div.fragment.nodes[0] else {
264            panic!("expected text in div");
265        };
266        assert_eq!(text.data.as_ref(), "X");
267    }
268}