1pub mod arena;
77pub mod ast;
78pub mod compat;
79pub mod cst;
80mod error;
81pub mod js;
82mod parse;
83mod primitives;
84mod source;
85
86pub use cst::{CstEdit, CstParser, Document, Language, parse_svelte, parse_svelte_incremental};
89
90pub use error::{CompileError, CompilerDiagnosticKind, SourceLocation, SourcePosition};
93
94pub use js::{ParsedJsExpression, ParsedJsProgram};
97
98pub 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
111pub 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 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 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 #[test]
183 fn verify_tree_sitter_changed_ranges_are_structural_only() {
184 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 assert!(
201 ranges.is_empty(),
202 "tree-sitter changed_ranges should be empty for same-structure edit, got: {ranges:?}"
203 );
204
205 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 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 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 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}