tiny_rsx/
parse.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use syn::{
4    braced,
5    ext::IdentExt as _,
6    parse::{Parse, ParseStream},
7    punctuated::Punctuated,
8    token, Ident, LitStr, Result, Token,
9};
10
11use crate::ast::{kw, Attr, DashIdent, Doctype, Node, NodeTree, Tag, Value};
12
13pub fn parse(input: TokenStream) -> Result<Box<[Node]>> {
14    Ok(syn::parse::<NodeTree>(input)?.nodes)
15}
16
17pub fn parse2(input: TokenStream2) -> Result<Box<[Node]>> {
18    Ok(syn::parse2::<NodeTree>(input)?.nodes)
19}
20
21impl Parse for DashIdent {
22    fn parse(input: ParseStream) -> Result<Self> {
23        // Parse a non-empty sequence of identifiers separated by dashes.
24        let inner =
25            Punctuated::<Ident, Token![-]>::parse_separated_nonempty_with(
26                input,
27                Ident::parse_any,
28            )?;
29
30        Ok(DashIdent(inner))
31    }
32}
33
34impl Parse for Doctype {
35    fn parse(input: ParseStream) -> Result<Self> {
36        Ok(Doctype {
37            lt_sign: input.parse()?,
38            excl_mark: input.parse()?,
39            doctype: input.parse()?,
40            html: input.parse()?,
41            gt_sign: input.parse()?,
42        })
43    }
44}
45
46impl Parse for Value {
47    fn parse(input: ParseStream) -> Result<Self> {
48        let lookahead = input.lookahead1();
49        if lookahead.peek(LitStr) {
50            Ok(Value::LitStr(input.parse()?))
51        } else if lookahead.peek(token::Brace) {
52            let content;
53            braced!(content in input);
54            Ok(Value::Expr(content.parse()?))
55        } else {
56            Err(lookahead.error())
57        }
58    }
59}
60
61impl Parse for Attr {
62    fn parse(input: ParseStream) -> Result<Self> {
63        Ok(Attr {
64            key: input.parse()?,
65            eq_sign: input.parse()?,
66            value: input.parse()?,
67        })
68    }
69}
70
71impl Parse for Tag {
72    fn parse(input: ParseStream) -> Result<Self> {
73        let lt_sign = input.parse()?;
74
75        if input.parse::<Option<Token![/]>>()?.is_some() {
76            let name = input.parse()?;
77            let gt_sign = input.parse()?;
78
79            return Ok(Tag::Closing {
80                lt_sign,
81                name,
82                gt_sign,
83            });
84        }
85
86        let name = input.parse()?;
87
88        let mut attrs = Vec::new();
89        while !(input.peek(Token![>])
90            || (input.peek(Token![/]) && input.peek2(Token![>])))
91        {
92            attrs.push(input.parse()?);
93        }
94
95        let void_slash = input.parse()?;
96        let gt_sign = input.parse()?;
97
98        Ok(Tag::Opening {
99            lt_sign,
100            name,
101            attrs,
102            void_slash,
103            gt_sign,
104        })
105    }
106}
107
108impl Parse for Node {
109    fn parse(input: ParseStream) -> Result<Self> {
110        let lookahead = input.lookahead1();
111        if lookahead.peek(Token![<])
112            && input.peek2(Token![!])
113            && input.peek3(kw::DOCTYPE)
114        {
115            Ok(Node::Doctype(input.parse()?))
116        } else if lookahead.peek(Token![<]) {
117            Ok(Node::Tag(input.parse()?))
118        } else if lookahead.peek(LitStr) || lookahead.peek(token::Brace) {
119            Ok(Node::Value(input.parse()?))
120        } else {
121            Err(lookahead.error())
122        }
123    }
124}
125
126impl Parse for NodeTree {
127    fn parse(input: ParseStream) -> Result<Self> {
128        let mut nodes = Vec::new();
129        let mut stack = Vec::new();
130
131        while !input.is_empty() {
132            let curr = input.parse()?;
133            let other = stack.last().and_then(|i| nodes.get(*i));
134
135            match (&curr, other) {
136                (
137                    Node::Tag(Tag::Opening {
138                        void_slash: None, ..
139                    }),
140                    _,
141                ) => {
142                    nodes.push(curr);
143                    stack.push(nodes.len() - 1);
144                }
145                (
146                    Node::Tag(Tag::Closing { name, .. }),
147                    Some(Node::Tag(Tag::Opening {
148                        name: other_name,
149                        void_slash: None,
150                        ..
151                    })),
152                ) => {
153                    if name != other_name {
154                        return Err(syn::Error::new_spanned(
155                            &curr,
156                            format_args!(
157                                "closing tag mismatch, expected \
158                                 </{other_name}> found </{name}>"
159                            ),
160                        ));
161                    }
162
163                    nodes.push(curr);
164                    stack.pop();
165                }
166                (Node::Tag(Tag::Closing { .. }), None) => {
167                    return Err(syn::Error::new_spanned(
168                        &curr,
169                        "missing opening tag",
170                    ));
171                }
172                _ => {
173                    nodes.push(curr);
174                }
175            }
176        }
177
178        if let Some(node) = stack.last().and_then(|i| nodes.get(*i)) {
179            return Err(syn::Error::new_spanned(node, "missing closing tag"));
180        }
181
182        Ok(Self {
183            nodes: nodes.into_boxed_slice(),
184        })
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use proc_macro2::Span;
191    use quote::quote;
192
193    use super::*;
194
195    macro_rules! dash_ident {
196        ($($tt:tt)*) => {
197            syn::parse2::<DashIdent>(quote!($($tt)*)).unwrap()
198        };
199    }
200
201    macro_rules! lit_str {
202        ($lit:literal) => {
203            syn::LitStr::new($lit, Span::call_site())
204        };
205    }
206
207    macro_rules! lit_bool {
208        ($lit:literal) => {
209            syn::LitBool::new($lit, Span::call_site())
210        };
211    }
212
213    #[test]
214    fn parses_to_doctype() {
215        assert_eq!(
216            syn::parse2::<Doctype>(quote!(<!DOCTYPE html>)).unwrap(),
217            Doctype {
218                lt_sign: Token![<](Span::call_site()),
219                excl_mark: Token![!](Span::call_site()),
220                doctype: kw::DOCTYPE(Span::call_site()),
221                html: kw::html(Span::call_site()),
222                gt_sign: Token![>](Span::call_site()),
223            }
224        )
225    }
226
227    #[test]
228    fn parses_to_string_value() {
229        assert_eq!(
230            syn::parse2::<Value>(quote!("foo")).unwrap(),
231            Value::LitStr(lit_str!("foo")),
232        );
233        assert_eq!(
234            syn::parse2::<Value>(quote!("")).unwrap(),
235            Value::LitStr(lit_str!("")),
236        );
237    }
238
239    #[test]
240    fn parses_to_expr_value() {
241        assert_eq!(
242            syn::parse2::<Value>(quote!({ true })).unwrap(),
243            Value::Expr(syn::Expr::Lit(syn::ExprLit {
244                attrs: vec![],
245                lit: syn::Lit::Bool(lit_bool!(true))
246            })),
247        );
248    }
249
250    #[test]
251    fn parses_to_opening_tag() {
252        assert_eq!(
253            syn::parse2::<Tag>(quote! ( <foo> )).unwrap(),
254            Tag::Opening {
255                lt_sign: token::Lt(Span::call_site()),
256                name: dash_ident!(foo),
257                attrs: vec![],
258                void_slash: None,
259                gt_sign: token::Gt(Span::call_site()),
260            }
261        )
262    }
263
264    #[test]
265    fn parses_to_opening_tag_with_attrs() {
266        assert_eq!(
267            syn::parse2::<Tag>(quote!(<foo bar="baz" qux={false}>)).unwrap(),
268            Tag::Opening {
269                lt_sign: token::Lt(Span::call_site()),
270                name: dash_ident!(foo),
271                attrs: vec![
272                    Attr {
273                        key: dash_ident!(bar),
274                        eq_sign: Token![=](Span::call_site()),
275                        value: Value::LitStr(lit_str!("baz"))
276                    },
277                    Attr {
278                        key: dash_ident!(qux),
279                        eq_sign: Token![=](Span::call_site()),
280                        value: Value::Expr(syn::Expr::Lit(syn::ExprLit {
281                            attrs: vec![],
282                            lit: syn::Lit::Bool(lit_bool!(false))
283                        }))
284                    },
285                ],
286                void_slash: None,
287                gt_sign: token::Gt(Span::call_site()),
288            }
289        )
290    }
291
292    #[test]
293    fn parses_to_void_tag() {
294        assert_eq!(
295            syn::parse2::<Tag>(quote!(<foo />)).unwrap(),
296            Tag::Opening {
297                lt_sign: token::Lt(Span::call_site()),
298                name: dash_ident!(foo),
299                attrs: vec![],
300                void_slash: Some(syn::token::Slash(Span::call_site())),
301                gt_sign: token::Gt(Span::call_site()),
302            }
303        )
304    }
305}