svelte_syntax/parse/component/
elements.rs1use 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}