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