Skip to main content

svelte_syntax/parse/component/
mod.rs

1use std::collections::HashSet;
2use std::fmt;
3use std::sync::Arc;
4
5use camino::Utf8PathBuf;
6use html_escape::decode_html_entities as decode_html_entities_cow;
7use serde::{Deserialize, Serialize};
8use tree_sitter::{Node, Point};
9
10use crate::ast::Document;
11use crate::{CompileError, SourceId, LineColumn, SourceText};
12
13mod elements;
14mod legacy;
15pub(crate) mod modern;
16
17pub use elements::{
18    AttributeKind, ElementKind, SvelteElementKind, classify_attribute_name, classify_element_name,
19    is_component_name, is_custom_element_name, is_valid_component_name, is_valid_element_name,
20    is_void_element_name,
21};
22pub use legacy::parse_root as parse_legacy_root_from_cst;
23pub use legacy::legacy_root_from_modern;
24pub(crate) use legacy::{
25    find_first_named_child, parse_identifier_name, parse_modern_attributes,
26    line_column_from_point, text_for_node,
27};
28pub(crate) use modern::parse_root as parse_root_from_cst;
29pub(crate) use modern::parse_root_incremental as parse_root_incremental_from_cst;
30pub(crate) use modern::{
31    attach_leading_comments_to_expression, attach_trailing_comments_to_expression,
32    find_matching_brace_close, line_column_at_offset, modern_empty_identifier_expression,
33    named_children_vec, parse_modern_expression_from_text, parse_modern_expression_tag,
34};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
37#[serde(rename_all = "kebab-case")]
38/// Selects which public AST shape the parser should return.
39pub enum ParseMode {
40    #[default]
41    Legacy,
42    Modern,
43}
44
45impl ParseMode {
46    #[must_use]
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::Legacy => "legacy",
50            Self::Modern => "modern",
51        }
52    }
53}
54
55impl fmt::Display for ParseMode {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str(self.as_str())
58    }
59}
60
61impl std::str::FromStr for ParseMode {
62    type Err = ();
63
64    fn from_str(value: &str) -> Result<Self, Self::Err> {
65        match value {
66            "legacy" => Ok(Self::Legacy),
67            "modern" => Ok(Self::Modern),
68            _ => Err(()),
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
74#[serde(default)]
75/// Options for parsing Svelte source into the public AST.
76pub struct ParseOptions {
77    /// Optional source filename used in diagnostics.
78    pub filename: Option<Utf8PathBuf>,
79    /// Optional project root used by path-sensitive tooling.
80    pub root_dir: Option<Utf8PathBuf>,
81    /// Compatibility flag matching Svelte's JavaScript API.
82    pub modern: Option<bool>,
83    /// Preferred AST shape when `modern` is not set.
84    pub mode: ParseMode,
85    /// Return a best-effort AST for malformed input when possible.
86    pub loose: bool,
87}
88
89impl ParseOptions {
90    #[must_use]
91    pub fn effective_mode(&self) -> ParseMode {
92        match self.modern {
93            Some(true) => ParseMode::Modern,
94            Some(false) => ParseMode::Legacy,
95            None => self.mode,
96        }
97    }
98}
99
100struct SvelteParserCore<'src> {
101    source: &'src str,
102    source_filename: Option<Utf8PathBuf>,
103    options: ParseOptions,
104}
105
106impl<'src> SvelteParserCore<'src> {
107    fn new(source: &'src str, options: ParseOptions) -> Self {
108        Self {
109            source,
110            source_filename: options.filename.clone(),
111            options,
112        }
113    }
114
115    fn parse_root(&self, root: Node<'_>) -> crate::ast::Root {
116        match self.options.effective_mode() {
117            ParseMode::Legacy => crate::ast::Root::Legacy(parse_legacy_root_from_cst(
118                self.source,
119                root,
120                self.options.loose,
121            )),
122            ParseMode::Modern => {
123                crate::ast::Root::Modern(parse_root_from_cst(self.source, root, self.options.loose))
124            }
125        }
126    }
127
128    fn parse(self) -> Result<Document, CompileError> {
129        let source_text = SourceText::new(
130            SourceId::new(0),
131            self.source,
132            self.source_filename.as_deref(),
133        );
134        let cst = crate::cst::parse_svelte(source_text)?;
135        Ok(Document {
136            root: self.parse_root(cst.root_node()),
137            source: Arc::from(self.source),
138        })
139    }
140}
141
142/// Parse a Svelte component into the public AST.
143///
144/// This matches the shape of Svelte's `parse(...)` API and can return either
145/// the legacy or modern AST.
146pub fn parse(source: &str, options: ParseOptions) -> Result<Document, CompileError> {
147    SvelteParserCore::new(source, options).parse()
148}
149
150/// Parse a Svelte component directly into the modern AST root.
151pub fn parse_modern_root(source: &str) -> Result<crate::ast::modern::Root, CompileError> {
152    let source_text = SourceText::new(SourceId::new(0), source, None);
153    let cst = crate::cst::parse_svelte(source_text)?;
154    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
155        parse_root_from_cst(source_text.text, cst.root_node(), false)
156    }))
157    .map_err(|_| CompileError::internal("failed to parse component root from cst"))
158}
159
160/// Detailed timing breakdown of the parse pipeline.
161#[derive(Debug, Clone)]
162pub struct ParseTimings {
163    /// tree-sitter GLR parse (source → Tree)
164    pub tree_sitter_parse_us: u64,
165    /// CST walk → modern AST
166    pub cst_to_ast_us: u64,
167    /// Expression enrichment (ESTree JSON with loc fields)
168    pub enrich_expressions_us: u64,
169    /// Total
170    pub total_us: u64,
171}
172
173/// Parse with detailed timing for each sub-phase.
174///
175/// Does NOT call `enrich_expressions` — caller is responsible for that if needed.
176pub fn parse_modern_root_timed(source: &str) -> Result<(crate::ast::modern::Root, ParseTimings), CompileError> {
177    use std::time::Instant;
178
179    let t0 = Instant::now();
180    let source_text = SourceText::new(SourceId::new(0), source, None);
181    let cst = crate::cst::parse_svelte(source_text)?;
182    let tree_sitter_us = t0.elapsed().as_micros() as u64;
183
184    let t1 = Instant::now();
185    let root = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
186        parse_root_from_cst(source_text.text, cst.root_node(), false)
187    }))
188    .map_err(|_| CompileError::internal("failed to parse component root from cst"))?;
189    let cst_to_ast_us = t1.elapsed().as_micros() as u64;
190
191    let total_us = t0.elapsed().as_micros() as u64;
192
193    Ok((root, ParseTimings {
194        tree_sitter_parse_us: tree_sitter_us,
195        cst_to_ast_us,
196        enrich_expressions_us: 0,
197        total_us,
198    }))
199}
200
201/// Parse a Svelte component into the modern AST root incrementally, reusing
202/// unchanged subtrees from a previous parse. Requires a previous CST document
203/// and the CST edit that was applied so tree-sitter can compute changed ranges.
204///
205/// Falls back to a full parse if the CST reports an error root.
206pub fn parse_modern_root_incremental(
207    source: &str,
208    old_source: &str,
209    old_root: &crate::ast::modern::Root,
210    old_cst: &crate::cst::Document<'_>,
211    edit: crate::cst::CstEdit,
212) -> Result<crate::ast::modern::Root, CompileError> {
213    use crate::cst;
214
215    let source_text = SourceText::new(SourceId::new(0), source, None);
216
217    // Build an edited copy of the old CST for two purposes:
218    // 1. tree-sitter incremental parsing (`parser.parse(new_src, Some(&edited_old))`)
219    // 2. computing changed_ranges (`edited_old.changed_ranges(&new_tree)`)
220    let mut edited_old_cst = old_cst.clone_for_incremental();
221    edited_old_cst.apply_edit(edit);
222
223    let new_cst = cst::parse_svelte_with_old_tree(source_text, &edited_old_cst)?;
224    let changed_ranges = new_cst.changed_ranges(&edited_old_cst);
225
226    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
227        parse_root_incremental_from_cst(
228            source_text.text,
229            new_cst.root_node(),
230            false,
231            old_root,
232            old_source,
233            &changed_ranges,
234        )
235    }))
236    .map_err(|_| CompileError::internal("failed to incrementally parse component root from cst"))
237}