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 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),
219 excl_mark: Token),
220 doctype: kw::DOCTYPE(Span::call_site()),
221 html: kw::html(Span::call_site()),
222 gt_sign: Token),
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),
275 value: Value::LitStr(lit_str!("baz"))
276 },
277 Attr {
278 key: dash_ident!(qux),
279 eq_sign: Token),
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}