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(crate) use legacy::parse_root as parse_legacy_root_from_cst;
23pub(crate) use legacy::{
24    find_first_named_child, parse_identifier_name, parse_modern_attributes,
25    line_column_from_point, text_for_node,
26};
27pub(crate) use modern::parse_root as parse_root_from_cst;
28pub(crate) use modern::parse_root_incremental as parse_root_incremental_from_cst;
29pub(crate) use modern::{
30    attach_leading_comments_to_expression, attach_trailing_comments_to_expression,
31    find_matching_brace_close, line_column_at_offset, modern_empty_identifier_expression,
32    named_children_vec, parse_modern_expression_from_text, parse_modern_expression_tag,
33};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
36#[serde(rename_all = "kebab-case")]
37/// Selects which public AST shape the parser should return.
38pub enum ParseMode {
39    #[default]
40    Legacy,
41    Modern,
42}
43
44impl ParseMode {
45    #[must_use]
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::Legacy => "legacy",
49            Self::Modern => "modern",
50        }
51    }
52}
53
54impl fmt::Display for ParseMode {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(self.as_str())
57    }
58}
59
60impl std::str::FromStr for ParseMode {
61    type Err = ();
62
63    fn from_str(value: &str) -> Result<Self, Self::Err> {
64        match value {
65            "legacy" => Ok(Self::Legacy),
66            "modern" => Ok(Self::Modern),
67            _ => Err(()),
68        }
69    }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
73#[serde(default)]
74/// Options for parsing Svelte source into the public AST.
75pub struct ParseOptions {
76    /// Optional source filename used in diagnostics.
77    pub filename: Option<Utf8PathBuf>,
78    /// Optional project root used by path-sensitive tooling.
79    pub root_dir: Option<Utf8PathBuf>,
80    /// Compatibility flag matching Svelte's JavaScript API.
81    pub modern: Option<bool>,
82    /// Preferred AST shape when `modern` is not set.
83    pub mode: ParseMode,
84    /// Return a best-effort AST for malformed input when possible.
85    pub loose: bool,
86}
87
88impl ParseOptions {
89    #[must_use]
90    pub fn effective_mode(&self) -> ParseMode {
91        match self.modern {
92            Some(true) => ParseMode::Modern,
93            Some(false) => ParseMode::Legacy,
94            None => self.mode,
95        }
96    }
97}
98
99struct SvelteParserCore<'src> {
100    source: &'src str,
101    source_filename: Option<Utf8PathBuf>,
102    options: ParseOptions,
103}
104
105impl<'src> SvelteParserCore<'src> {
106    fn new(source: &'src str, options: ParseOptions) -> Self {
107        Self {
108            source,
109            source_filename: options.filename.clone(),
110            options,
111        }
112    }
113
114    fn parse_root(&self, root: Node<'_>) -> crate::ast::Root {
115        match self.options.effective_mode() {
116            ParseMode::Legacy => crate::ast::Root::Legacy(parse_legacy_root_from_cst(
117                self.source,
118                root,
119                self.options.loose,
120            )),
121            ParseMode::Modern => {
122                crate::ast::Root::Modern(parse_root_from_cst(self.source, root, self.options.loose))
123            }
124        }
125    }
126
127    fn parse(self) -> Result<Document, CompileError> {
128        let source_text = SourceText::new(
129            SourceId::new(0),
130            self.source,
131            self.source_filename.as_deref(),
132        );
133        let cst = crate::cst::parse_svelte(source_text)?;
134        Ok(Document {
135            root: self.parse_root(cst.root_node()),
136            source: Arc::from(self.source),
137        })
138    }
139}
140
141/// Parse a Svelte component into the public AST.
142///
143/// This matches the shape of Svelte's `parse(...)` API and can return either
144/// the legacy or modern AST.
145pub fn parse(source: &str, options: ParseOptions) -> Result<Document, CompileError> {
146    SvelteParserCore::new(source, options).parse()
147}
148
149/// Parse a Svelte component directly into the modern AST root.
150pub fn parse_modern_root(source: &str) -> Result<crate::ast::modern::Root, CompileError> {
151    let source_text = SourceText::new(SourceId::new(0), source, None);
152    let cst = crate::cst::parse_svelte(source_text)?;
153    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
154        parse_root_from_cst(source_text.text, cst.root_node(), false)
155    }))
156    .map_err(|_| CompileError::internal("failed to parse component root from cst"))
157}
158
159/// Parse a Svelte component into the modern AST root incrementally, reusing
160/// unchanged subtrees from a previous parse. Requires a previous CST document
161/// and the CST edit that was applied so tree-sitter can compute changed ranges.
162///
163/// Falls back to a full parse if the CST reports an error root.
164pub fn parse_modern_root_incremental(
165    source: &str,
166    old_source: &str,
167    old_root: &crate::ast::modern::Root,
168    old_cst: &crate::cst::Document<'_>,
169    edit: crate::cst::CstEdit,
170) -> Result<crate::ast::modern::Root, CompileError> {
171    use crate::cst;
172
173    let source_text = SourceText::new(SourceId::new(0), source, None);
174
175    // Build an edited copy of the old CST for two purposes:
176    // 1. tree-sitter incremental parsing (`parser.parse(new_src, Some(&edited_old))`)
177    // 2. computing changed_ranges (`edited_old.changed_ranges(&new_tree)`)
178    let mut edited_old_cst = old_cst.clone_for_incremental();
179    edited_old_cst.apply_edit(edit);
180
181    let new_cst = cst::parse_svelte_with_old_tree(source_text, &edited_old_cst)?;
182    let changed_ranges = new_cst.changed_ranges(&edited_old_cst);
183
184    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
185        parse_root_incremental_from_cst(
186            source_text.text,
187            new_cst.root_node(),
188            false,
189            old_root,
190            old_source,
191            &changed_ranges,
192        )
193    }))
194    .map_err(|_| CompileError::internal("failed to incrementally parse component root from cst"))
195}