1use core::fmt;
2
3use proc_macro2::{Punct, Spacing, TokenStream};
4use quote::{ToTokens, TokenStreamExt};
5use syn::{ext::*, parse::ParseStream, spanned::Spanned, Ident, LitFloat, LitInt, LitStr, Token};
6
7#[rustfmt::skip]
8const fn is_invalid_attribute_char(ch: char) -> bool {
9 matches!(
10 ch,
11 '\0'..='\x1F' | '\x7F'..='\u{9F}'
12 | 'A'..='Z' | ' ' | '"' | '\'' | '>' | '/' | '='
13 | '\u{FDD0}'..='\u{FDEF}'
14 | '\u{0FFFE}' | '\u{0FFFF}' | '\u{01FFFE}' | '\u{01FFFF}' | '\u{2FFFE}'
15 | '\u{2FFFF}' | '\u{3FFFE}' | '\u{03FFFF}' | '\u{04FFFE}' | '\u{4FFFF}'
16 | '\u{5FFFE}' | '\u{5FFFF}' | '\u{06FFFE}' | '\u{06FFFF}' | '\u{7FFFE}'
17 | '\u{7FFFF}' | '\u{8FFFE}' | '\u{08FFFF}' | '\u{09FFFE}' | '\u{9FFFF}'
18 | '\u{AFFFE}' | '\u{AFFFF}' | '\u{0BFFFE}' | '\u{0BFFFF}' | '\u{CFFFE}'
19 | '\u{CFFFF}' | '\u{DFFFE}' | '\u{0DFFFF}' | '\u{0EFFFE}' | '\u{EFFFF}'
20 | '\u{FFFFE}' | '\u{FFFFF}' | '\u{10FFFE}' | '\u{10FFFF}'
21 )
22}
23
24#[rustfmt::skip]
25const fn is_invalid_tagname_char(ch: char) -> bool {
26 !matches!(
27 ch,
28 '-' | '.' | '0'..='9' | '_' | 'a'..='z'
29 | '\u{B7}' | '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{37D}'
30 | '\u{37F}'..='\u{1FFF}' | '\u{200C}'..='\u{200D}' | '\u{203F}'..='\u{2040}'
31 | '\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}'
32 | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | '\u{10000}'..='\u{EFFFF}'
33 )
34}
35
36const INVALID_TAG_MSG: &str =
37 "Invalid tag name (https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-name)";
38const INVALID_ATTR_MSG: &str = "Invalid attribute name \
39 (https://html.spec.whatwg.org/multipage/syntax.html#syntax-attribute-name)";
40
41#[derive(Debug, Clone)]
42pub enum NamePart {
43 Ident(Ident),
44 Int(LitInt),
45 Float(LitFloat),
46 Dot(Token![.], Spacing),
47 Hyphen(Token![-], Spacing),
48 Colon(Token![:], Spacing),
49 At(Token![@], Spacing),
50 Question(Token![?], Spacing),
51}
52
53impl ToTokens for NamePart {
54 fn to_tokens(&self, tokens: &mut TokenStream) {
55 match self {
56 Self::Ident(slf) => slf.to_tokens(tokens),
57 Self::Int(slf) => slf.to_tokens(tokens),
58 Self::Float(slf) => slf.to_tokens(tokens),
59 Self::Dot(slf, _) => slf.to_tokens(tokens),
60 Self::Hyphen(slf, _) => slf.to_tokens(tokens),
61 Self::Colon(slf, _) => slf.to_tokens(tokens),
62 Self::At(slf, _) => slf.to_tokens(tokens),
63 Self::Question(slf, _) => slf.to_tokens(tokens),
64 }
65 }
66}
67
68impl NamePart {
69 fn peek_attr(&self, input: ParseStream) -> bool {
70 let Some((token, _)) = input.cursor().token_tree() else {
71 return false;
72 };
73
74 if self
75 .span()
76 .join(token.span())
77 .and_then(|span| span.source_text())
78 .is_some_and(|source| source.contains(char::is_whitespace))
79 {
80 return false;
81 }
82
83 !matches!(self, Self::Ident(_) | Self::Int(_) | Self::Float(_))
84 && (input.peek(Ident::peek_any) || input.peek(LitInt) || input.peek(LitFloat))
85 || !matches!(
86 self,
87 Self::Dot(_, Spacing::Alone)
88 | Self::Hyphen(_, Spacing::Alone)
89 | Self::Colon(_, Spacing::Alone)
90 | Self::At(_, Spacing::Alone)
91 | Self::Question(_, Spacing::Alone)
92 ) && (input.peek(Token![.])
93 || input.peek(Token![-])
94 || input.peek(Token![:])
95 || input.peek(Token![@])
96 || input.peek(Token![?]) && !input.peek2(Token![=]))
97 }
98
99 fn peek_tag(&self, input: ParseStream) -> bool {
100 let Some((token, _)) = input.cursor().token_tree() else {
101 return false;
102 };
103
104 if self
105 .span()
106 .join(token.span())
107 .and_then(|span| span.source_text())
108 .is_some_and(|source| source.contains(char::is_whitespace))
109 {
110 return false;
111 }
112
113 const _: () = assert!(is_invalid_tagname_char('@'));
114 const _: () = assert!(is_invalid_tagname_char(':'));
115 const _: () = assert!(is_invalid_tagname_char('?'));
116
117 !matches!(self, Self::Ident(_) | Self::Int(_) | Self::Float(_))
118 && (input.peek(Ident::peek_any) || input.peek(LitInt) || input.peek(LitFloat))
119 || !matches!(
120 self,
121 Self::Dot(_, Spacing::Alone)
122 | Self::Hyphen(_, Spacing::Alone)
123 | Self::Colon(_, Spacing::Alone)
124 | Self::At(_, Spacing::Alone)
125 | Self::Question(_, Spacing::Alone)
126 ) && (input.peek(Token![.]) || input.peek(Token![-]))
127 }
128
129 pub fn parse_attr(input: ParseStream) -> syn::Result<Self> {
130 let lookahead1 = input.lookahead1();
131
132 let slf = if lookahead1.peek(Ident::peek_any) {
133 Self::Ident(Ident::parse_any(input)?)
134 } else if lookahead1.peek(LitInt) {
135 Self::Int(input.parse()?)
136 } else if lookahead1.peek(LitFloat) {
137 Self::Float(input.parse()?)
138 } else if lookahead1.peek(Token![.]) {
139 let punct: Punct = input.parse()?;
140 let mut tk: Token![.] = Default::default();
141 tk.spans[0] = punct.span();
142 Self::Dot(tk, punct.spacing())
143 } else if lookahead1.peek(Token![-]) {
144 let punct: Punct = input.parse()?;
145 let mut tk: Token![-] = Default::default();
146 tk.spans[0] = punct.span();
147 Self::Hyphen(tk, punct.spacing())
148 } else if lookahead1.peek(Token![:]) {
149 let punct: Punct = input.parse()?;
150 let mut tk: Token![:] = Default::default();
151 tk.spans[0] = punct.span();
152 Self::Colon(tk, punct.spacing())
153 } else if lookahead1.peek(Token![@]) {
154 let punct: Punct = input.parse()?;
155 let mut tk: Token![@] = Default::default();
156 tk.spans[0] = punct.span();
157 Self::At(tk, punct.spacing())
158 } else if lookahead1.peek(Token![?]) {
159 let punct: Punct = input.parse()?;
160 let mut tk: Token![?] = Default::default();
161 tk.spans[0] = punct.span();
162 Self::Question(tk, punct.spacing())
163 } else {
164 return Err(lookahead1.error());
165 };
166
167 if slf.to_string().contains(is_invalid_attribute_char) {
168 Err(syn::Error::new_spanned(slf, INVALID_ATTR_MSG))
169 } else {
170 Ok(slf)
171 }
172 }
173
174 pub fn parse_tag_first(input: ParseStream) -> syn::Result<Self> {
175 let ident = Ident::parse_any(input)?;
176
177 let s = ident.to_string();
178 if s.starts_with(|ch: char| !ch.is_ascii_lowercase()) || s.contains(is_invalid_tagname_char)
179 {
180 Err(syn::Error::new(ident.span(), INVALID_TAG_MSG))
181 } else {
182 Ok(Self::Ident(ident))
183 }
184 }
185
186 pub fn parse_tag(input: ParseStream) -> syn::Result<Self> {
187 let lookahead1 = input.lookahead1();
188
189 let slf = if lookahead1.peek(Ident::peek_any) {
190 Self::Ident(Ident::parse_any(input)?)
191 } else if lookahead1.peek(LitInt) {
192 Self::Int(input.parse()?)
193 } else if lookahead1.peek(LitFloat) {
194 Self::Float(input.parse()?)
195 } else if lookahead1.peek(Token![.]) {
196 let punct: Punct = input.parse()?;
197 let mut tk: Token![.] = Default::default();
198 tk.spans[0] = punct.span();
199 Self::Dot(tk, punct.spacing())
200 } else if lookahead1.peek(Token![-]) {
201 let punct: Punct = input.parse()?;
202 let mut tk: Token![-] = Default::default();
203 tk.spans[0] = punct.span();
204 Self::Hyphen(tk, punct.spacing())
205 } else {
206 return Err(lookahead1.error());
207 };
208
209 if slf.to_string().contains(is_invalid_tagname_char) {
210 Err(syn::Error::new_spanned(slf, INVALID_TAG_MSG))
211 } else {
212 Ok(slf)
213 }
214 }
215}
216
217impl fmt::Display for NamePart {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 match self {
220 NamePart::Ident(part) => fmt::Display::fmt(part, f),
221 NamePart::Int(part) => fmt::Display::fmt(part, f),
222 NamePart::Float(part) => fmt::Display::fmt(part, f),
223 NamePart::Dot(_, _) => f.write_str("."),
224 NamePart::Hyphen(_, _) => f.write_str("-"),
225 NamePart::Colon(_, _) => f.write_str(":"),
226 NamePart::At(_, _) => f.write_str("@"),
227 NamePart::Question(_, _) => f.write_str("?"),
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
233pub enum Name {
234 Str(LitStr),
235 Parts(Vec<NamePart>),
236}
237
238impl fmt::Display for Name {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match self {
241 Self::Str(s) => fmt::Display::fmt(&s.value(), f),
242 Self::Parts(parts) => {
243 for part in parts {
244 fmt::Display::fmt(part, f)?;
245 }
246 Ok(())
247 }
248 }
249 }
250}
251
252impl Name {
253 pub fn parse_tag(input: ParseStream) -> syn::Result<Self> {
254 if input.peek(LitStr) {
255 let lit: LitStr = input.parse()?;
256 let value = lit.value();
257 if value.starts_with(|ch: char| !ch.is_ascii_lowercase())
258 || value.contains(is_invalid_tagname_char)
259 {
260 return Err(syn::Error::new(lit.span(), INVALID_TAG_MSG));
261 }
262 Ok(Self::Str(lit))
263 } else {
264 let mut parts = vec![NamePart::parse_tag_first(input)?];
265 while parts.last().unwrap().peek_tag(input) {
266 parts.push(NamePart::parse_tag(input)?);
267 }
268 Ok(Self::Parts(parts))
269 }
270 }
271 pub fn parse_attr(input: ParseStream) -> syn::Result<Self> {
272 if input.peek(LitStr) {
273 let lit: LitStr = input.parse()?;
274 let value = lit.value();
275 if value.contains(is_invalid_attribute_char) {
276 return Err(syn::Error::new(lit.span(), INVALID_ATTR_MSG));
277 }
278 Ok(Self::Str(lit))
279 } else {
280 let mut parts = vec![NamePart::parse_attr(input)?];
281 while parts.last().unwrap().peek_attr(input) {
282 parts.push(NamePart::parse_attr(input)?);
283 }
284 Ok(Self::Parts(parts))
285 }
286 }
287}
288
289impl ToTokens for Name {
290 fn to_tokens(&self, tokens: &mut TokenStream) {
291 match self {
292 Self::Str(slf) => slf.to_tokens(tokens),
293 Self::Parts(parts) => {
294 tokens.append_all(parts);
295 }
296 }
297 }
298}
299
300impl PartialEq for Name {
301 fn eq(&self, other: &Self) -> bool {
302 self.to_string() == other.to_string()
303 }
304}
305impl Eq for Name {}
306
307#[test]
308fn test_name() {
309 use quote::quote;
310 use syn::parse::Parser;
311
312 let name = Name::parse_attr
313 .parse2(quote! {
314 @on::hello-world.camel
315 })
316 .unwrap();
317 let name2 = Name::parse_attr
318 .parse2(quote! {
319 "@on::hello-world.camel"
320 })
321 .unwrap();
322 assert_eq!(name, name2);
323}