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, DiagnosticKind, LineColumn, SourcePosition};
93
94pub use js::{JsExpression, JsProgram};
97
98pub use parse::{
101 AttributeKind, ElementKind, ParseMode, ParseOptions, SvelteElementKind,
102 classify_attribute_name, classify_element_name, find_matching_brace_close, is_component_name,
103 is_custom_element_name, is_valid_component_name, is_valid_element_name, is_void_element_name,
104 line_column_at_offset, parse, parse_css, parse_modern_css_nodes,
105 parse_modern_expression_from_text, parse_modern_expression_tag, parse_modern_root,
106 parse_modern_root_incremental, parse_svelte_ignores,
107};
108
109pub use primitives::{BytePos, SourceId, Span};
112pub use source::SourceText;
113
114#[cfg(test)]
115mod tests {
116 use std::sync::Arc;
117
118 use crate::ast::modern::Node;
119 use crate::cst::{CstEdit, parse_svelte};
120 use crate::parse_modern_root;
121 use crate::primitives::SourceId;
122 use crate::source::SourceText;
123
124 #[test]
125 fn modern_root_scripts_and_template_expressions_keep_oxc_handles() {
126 let root = parse_modern_root("<script>let count = 0;</script><button>{count + 1}</button>")
127 .expect("modern root should parse");
128
129 let instance = root.instance.as_ref().expect("instance script");
130 assert_eq!(instance.oxc_program().body.len(), 1);
131
132 let Node::RegularElement(element) = &root.fragment.nodes[0] else {
133 panic!("expected regular element");
134 };
135 let Node::ExpressionTag(tag) = &element.fragment.nodes[0] else {
136 panic!("expected expression tag");
137 };
138 assert!(tag.expression.parsed().is_some());
139 assert!(tag.expression.oxc_expression().is_some());
140 }
141
142 #[test]
143 fn incremental_parse_reuses_unchanged_script() {
144 let before = "<script>let count = 0;</script>\n<div>Hello</div>";
145 let after = "<script>let count = 0;</script>\n<div>World</div>";
146 let edit_start = "<script>let count = 0;</script>\n<div>".len();
147 let edit_old_end = "<script>let count = 0;</script>\n<div>Hello".len();
148
149 let old_root = parse_modern_root(before).expect("initial parse");
150
151 let before_src = SourceText::new(SourceId::new(1), before, None);
152 let old_cst = parse_svelte(before_src).expect("initial CST parse");
153
154 let edit = CstEdit::replace(before, edit_start, edit_old_end, "World");
155
156 let new_root = crate::parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)
157 .expect("incremental parse");
158
159 let old_script = old_root.instance.as_ref().expect("old instance");
161 let new_script = new_root.instance.as_ref().expect("new instance");
162 assert!(
163 Arc::ptr_eq(&old_script.content, &new_script.content),
164 "unchanged script should be Arc-reused (same pointer)",
165 );
166
167 let el_node = new_root.fragment.nodes.iter().find(|n| matches!(n, Node::RegularElement(_)))
170 .expect("expected regular element in new root fragment");
171 let Node::RegularElement(new_el) = el_node else { unreachable!() };
172 let Node::Text(text) = &new_el.fragment.nodes[0] else {
173 panic!("expected text node in element fragment");
174 };
175 assert_eq!(text.data.as_ref(), "World");
176 }
177
178 #[test]
181 fn verify_tree_sitter_changed_ranges_are_structural_only() {
182 let before = "<div>A</div>";
184 let after = "<div>X</div>";
185
186 let before_src = SourceText::new(SourceId::new(50), before, None);
187 let old_cst = parse_svelte(before_src).expect("cst");
188 let mut edited = old_cst.clone_for_incremental();
189 let edit = CstEdit::replace(before, 5, 6, "X");
190 edited.apply_edit(edit);
191
192 let after_src = SourceText::new(SourceId::new(51), after, None);
193 let new_cst = crate::cst::parse_svelte_with_old_tree(after_src, &edited).expect("cst");
194 let ranges = new_cst.changed_ranges(&edited);
195
196 assert!(
199 ranges.is_empty(),
200 "tree-sitter changed_ranges should be empty for same-structure edit, got: {ranges:?}"
201 );
202
203 let before2 = "<div>A</div>";
205 let after2 = "<div>A</div><span>B</span>";
206 let before_src2 = SourceText::new(SourceId::new(52), before2, None);
207 let old_cst2 = parse_svelte(before_src2).expect("cst");
208 let mut edited2 = old_cst2.clone_for_incremental();
209 let edit2 = CstEdit::insert(before2, before2.len(), "<span>B</span>");
210 edited2.apply_edit(edit2);
211
212 let after_src2 = SourceText::new(SourceId::new(53), after2, None);
213 let new_cst2 = crate::cst::parse_svelte_with_old_tree(after_src2, &edited2).expect("cst");
214 let ranges2 = new_cst2.changed_ranges(&edited2);
215
216 assert!(
218 !ranges2.is_empty(),
219 "tree-sitter changed_ranges should be non-empty for structural edit"
220 );
221 }
222
223 #[test]
224 fn incremental_parse_reuses_unchanged_sibling_element() {
225 let before = "<div>A</div><span>B</span>";
226 let after = "<div>X</div><span>B</span>";
227 let edit_start = "<div>".len();
228 let edit_old_end = "<div>A".len();
229
230 let old_root = parse_modern_root(before).expect("initial parse");
231
232 let before_src = SourceText::new(SourceId::new(3), before, None);
233 let old_cst = parse_svelte(before_src).expect("initial CST parse");
234
235 let edit = CstEdit::replace(before, edit_start, edit_old_end, "X");
236
237 let new_root = crate::parse_modern_root_incremental(after, before, &old_root, &old_cst, edit)
238 .expect("incremental parse");
239
240 assert_eq!(new_root.fragment.nodes.len(), 2);
242 let Node::RegularElement(new_span) = &new_root.fragment.nodes[1] else {
243 panic!("expected span element");
244 };
245 assert_eq!(new_span.name.as_ref(), "span");
246
247 let Node::RegularElement(new_div) = &new_root.fragment.nodes[0] else {
249 panic!("expected div element");
250 };
251 let Node::Text(text) = &new_div.fragment.nodes[0] else {
252 panic!("expected text in div");
253 };
254 assert_eq!(text.data.as_ref(), "X");
255 }
256}