Skip to main content

svelte_syntax/parse/component/
elements.rs

1use std::str::FromStr;
2use unicode_ident::{is_xid_continue, is_xid_start};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ElementKind {
6    Script,
7    Style,
8    Slot,
9    Template,
10    Textarea,
11    Svelte(SvelteElementKind),
12    Other,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum AttributeKind {
17    This,
18    Other,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SvelteElementKind {
23    Head,
24    Options,
25    Window,
26    Document,
27    Body,
28    Element,
29    Component,
30    SelfTag,
31    Fragment,
32    Boundary,
33    Unknown,
34}
35
36impl SvelteElementKind {
37    #[must_use]
38    pub const fn is_known(self) -> bool {
39        !matches!(self, Self::Unknown)
40    }
41}
42
43impl FromStr for SvelteElementKind {
44    type Err = ();
45
46    fn from_str(value: &str) -> Result<Self, Self::Err> {
47        match value {
48            "head" => Ok(Self::Head),
49            "options" => Ok(Self::Options),
50            "window" => Ok(Self::Window),
51            "document" => Ok(Self::Document),
52            "body" => Ok(Self::Body),
53            "element" => Ok(Self::Element),
54            "component" => Ok(Self::Component),
55            "self" => Ok(Self::SelfTag),
56            "fragment" => Ok(Self::Fragment),
57            "boundary" => Ok(Self::Boundary),
58            _ => Err(()),
59        }
60    }
61}
62
63pub fn classify_element_name(name: &str) -> ElementKind {
64    match name {
65        "script" => ElementKind::Script,
66        "style" => ElementKind::Style,
67        "slot" => ElementKind::Slot,
68        "template" => ElementKind::Template,
69        "textarea" => ElementKind::Textarea,
70        _ => match name.strip_prefix("svelte:") {
71            Some(name) => ElementKind::Svelte(name.parse().unwrap_or(SvelteElementKind::Unknown)),
72            None => ElementKind::Other,
73        },
74    }
75}
76
77pub fn classify_attribute_name(name: &str) -> AttributeKind {
78    match name {
79        "this" => AttributeKind::This,
80        _ => AttributeKind::Other,
81    }
82}
83
84pub fn is_component_name(name: &str) -> bool {
85    let mut chars = name.chars();
86    let Some(first) = chars.next() else {
87        return false;
88    };
89
90    first.is_uppercase() || (is_ident_start(first) && name.contains('.'))
91}
92
93pub fn is_custom_element_name(name: &str) -> bool {
94    name.contains('-')
95}
96
97pub fn is_valid_component_name(name: &str) -> bool {
98    let Some((head, tail)) = name.split_once('.') else {
99        let mut chars = name.chars();
100        let Some(first) = chars.next() else {
101            return false;
102        };
103
104        return first.is_uppercase() && chars.all(is_component_char);
105    };
106
107    is_identifier(head)
108        && !tail.is_empty()
109        && tail
110            .split('.')
111            .all(|segment| !segment.is_empty() && segment.chars().all(is_component_char))
112}
113
114pub fn is_valid_element_name(name: &str) -> bool {
115    is_doctype_name(name) || is_meta_name(name) || is_tag_name(name)
116}
117
118pub fn is_void_element_name(name: &str) -> bool {
119    matches!(
120        name,
121        "area"
122            | "base"
123            | "br"
124            | "col"
125            | "embed"
126            | "hr"
127            | "img"
128            | "input"
129            | "keygen"
130            | "link"
131            | "meta"
132            | "param"
133            | "source"
134            | "track"
135            | "wbr"
136    )
137}
138
139fn is_doctype_name(name: &str) -> bool {
140    let Some(rest) = name.strip_prefix('!') else {
141        return false;
142    };
143
144    !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_alphabetic())
145}
146
147fn is_meta_name(name: &str) -> bool {
148    let Some((namespace, local)) = name.split_once(':') else {
149        return false;
150    };
151
152    is_ascii_alnum_ident(namespace) && is_meta_local_name(local)
153}
154
155fn is_tag_name(name: &str) -> bool {
156    let mut chars = name.chars().peekable();
157    let Some(first) = chars.next() else {
158        return false;
159    };
160    if !first.is_ascii_alphabetic() {
161        return false;
162    }
163
164    while chars.next_if(|ch| ch.is_ascii_alphanumeric()).is_some() {}
165
166    while chars.next_if_eq(&'-').is_some() {
167        let mut segment = 0;
168        while chars.next_if(|ch| is_tag_char(*ch)).is_some() {
169            segment += 1;
170        }
171        if segment == 0 {
172            return false;
173        }
174    }
175
176    chars.next().is_none()
177}
178
179fn is_identifier(text: &str) -> bool {
180    let mut chars = text.chars();
181    let Some(first) = chars.next() else {
182        return false;
183    };
184
185    is_ident_start(first) && chars.all(is_component_char)
186}
187
188fn is_ascii_alnum_ident(text: &str) -> bool {
189    let mut chars = text.chars();
190    let Some(first) = chars.next() else {
191        return false;
192    };
193
194    first.is_ascii_alphabetic() && chars.all(|ch| ch.is_ascii_alphanumeric())
195}
196
197fn is_meta_local_name(text: &str) -> bool {
198    let mut chars = text.chars();
199    let Some(first) = chars.next() else {
200        return false;
201    };
202    if !first.is_ascii_alphabetic() {
203        return false;
204    }
205
206    let mut last = first;
207    for ch in chars {
208        if !(ch.is_ascii_alphanumeric() || ch == '-') {
209            return false;
210        }
211        last = ch;
212    }
213
214    last.is_ascii_alphanumeric()
215}
216
217fn is_ident_start(ch: char) -> bool {
218    ch == '$' || ch == '_' || is_xid_start(ch)
219}
220
221fn is_component_char(ch: char) -> bool {
222    ch == '$' || ch == '\u{200c}' || ch == '\u{200d}' || is_xid_continue(ch)
223}
224
225fn is_tag_char(ch: char) -> bool {
226    ch.is_ascii_alphanumeric()
227        || matches!(ch, '.' | '_' | '-')
228        || matches!(
229            ch,
230            '\u{00b7}'
231                | '\u{00c0}'..='\u{00d6}'
232                | '\u{00d8}'..='\u{00f6}'
233                | '\u{00f8}'..='\u{037d}'
234                | '\u{037f}'..='\u{1fff}'
235                | '\u{200c}'..='\u{200d}'
236                | '\u{203f}'..='\u{2040}'
237                | '\u{2070}'..='\u{218f}'
238                | '\u{2c00}'..='\u{2fef}'
239                | '\u{3001}'..='\u{d7ff}'
240                | '\u{f900}'..='\u{fdcf}'
241                | '\u{fdf0}'..='\u{fffd}'
242                | '\u{10000}'..='\u{effff}'
243        )
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn accepts_valid_component_names() {
252        assert!(is_valid_component_name("Component"));
253        assert!(is_valid_component_name("Wunderschön"));
254        assert!(is_valid_component_name("Namespace.Schön"));
255        assert!(is_valid_component_name("namespace.1"));
256    }
257
258    #[test]
259    fn rejects_invalid_component_names() {
260        assert!(!is_valid_component_name("Components[1]"));
261        assert!(!is_valid_component_name("Namespace."));
262        assert!(!is_valid_component_name(".Component"));
263    }
264
265    #[test]
266    fn accepts_valid_element_names() {
267        assert!(is_valid_element_name("div"));
268        assert!(is_valid_element_name("foreignObject"));
269        assert!(is_valid_element_name("math-α"));
270        assert!(is_valid_element_name("svelte:head"));
271        assert!(is_valid_element_name("!DOCTYPE"));
272    }
273
274    #[test]
275    fn rejects_invalid_element_names() {
276        assert!(!is_valid_element_name("yes[no]"));
277        assert!(!is_valid_element_name("svelte:"));
278        assert!(!is_valid_element_name("1div"));
279    }
280}