templr_parser/
name.rs

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}