1extern crate proc_macro;
2
3use proc_macro2::{Span, TokenStream};
4use quote::{quote, ToTokens};
5use std::convert::TryFrom;
6use syn::{
7 parse::{Parse, ParseStream},
8 parse_macro_input,
9 punctuated::Punctuated,
10 spanned::Spanned,
11 token::Comma,
12};
13use syn_rsx::{NodeName, NodeType};
14
15#[proc_macro]
16pub fn mox(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
17 let item = parse_macro_input!(input as MoxItem);
18 quote!(#item).into()
19}
20
21enum MoxItem {
22 Tag(MoxTag),
23 Expr(MoxExpr),
24 None,
25}
26
27impl Parse for MoxItem {
28 fn parse(input: ParseStream) -> syn::Result<Self> {
29 fn parse_fmt_expr(parse_stream: ParseStream) -> syn::Result<Option<TokenStream>> {
30 if parse_stream.peek(syn::Token![%]) {
31 parse_stream.parse::<syn::Token![%]>()?;
32 let arguments: Punctuated<syn::Expr, Comma> =
33 Punctuated::parse_separated_nonempty(parse_stream)?;
34 if parse_stream.is_empty() {
35 Ok(Some(quote!(format_args!(#arguments))))
36 } else {
37 Err(parse_stream.error(format!("Expected the end, found `{}`", parse_stream)))
38 }
39 } else {
40 Ok(None)
41 }
42 }
43
44 let parse_config = syn_rsx::ParserConfig::new()
45 .transform_block(parse_fmt_expr)
46 .number_of_top_level_nodes(1);
47 let parser = syn_rsx::Parser::new(parse_config);
48 let node = parser.parse(input)?.remove(0);
49
50 MoxItem::try_from(node)
51 }
52}
53
54impl TryFrom<syn_rsx::Node> for MoxItem {
55 type Error = syn::Error;
56
57 fn try_from(node: syn_rsx::Node) -> syn::Result<Self> {
58 match node.node_type {
59 NodeType::Element => MoxTag::try_from(node).map(MoxItem::Tag),
60 NodeType::Attribute | NodeType::Fragment => Err(Self::node_convert_error(&node)),
61 NodeType::Text | NodeType::Block => MoxExpr::try_from(node).map(MoxItem::Expr),
62 NodeType::Comment | NodeType::Doctype => Ok(MoxItem::None),
63 }
64 }
65}
66
67impl ToTokens for MoxItem {
68 fn to_tokens(&self, tokens: &mut TokenStream) {
69 match self {
70 MoxItem::Tag(tag) => tag.to_tokens(tokens),
71 MoxItem::Expr(expr) => expr.to_tokens(tokens),
72 MoxItem::None => (),
73 }
74 }
75}
76
77struct MoxTag {
78 name: syn::ExprPath,
79 attributes: Vec<MoxAttr>,
80 children: Vec<MoxItem>,
81}
82
83impl TryFrom<syn_rsx::Node> for MoxTag {
84 type Error = syn::Error;
85
86 fn try_from(mut node: syn_rsx::Node) -> syn::Result<Self> {
87 match node.node_type {
88 NodeType::Element => Ok(Self {
89 name: MoxTag::validate_name(node.name.unwrap())?,
90 attributes: node
91 .attributes
92 .drain(..)
93 .map(|node| MoxAttr::try_from(node))
94 .collect::<syn::Result<Vec<_>>>()?,
95 children: node
96 .children
97 .drain(..)
98 .map(|node| MoxItem::try_from(node))
99 .collect::<syn::Result<Vec<_>>>()?,
100 }),
101 NodeType::Attribute
102 | NodeType::Text
103 | NodeType::Block
104 | NodeType::Comment
105 | NodeType::Doctype
106 | NodeType::Fragment => Err(Self::node_convert_error(&node)),
108 }
109 }
110}
111
112impl MoxTag {
113 fn validate_name(name: syn_rsx::NodeName) -> syn::Result<syn::ExprPath> {
114 match name {
115 NodeName::Path(mut expr_path) => {
116 mangle_expr_path(&mut expr_path);
117 Ok(expr_path)
118 }
119 NodeName::Dash(punctuated) => {
120 Err(syn::Error::new(punctuated.span(), "Dash tag name syntax isn't supported"))
122 }
123 NodeName::Colon(punctuated) => {
124 Err(syn::Error::new(punctuated.span(), "Colon tag name syntax isn't supported"))
125 }
126 NodeName::Block(block) => {
127 Err(syn::Error::new(block.span(), "Block expression as a tag name isn't supported"))
128 }
129 }
130 }
131}
132
133impl ToTokens for MoxTag {
134 fn to_tokens(&self, tokens: &mut TokenStream) {
135 let MoxTag { name, attributes, children } = self;
136
137 let mut contents = quote!();
140
141 for attr in attributes {
142 attr.to_tokens(&mut contents);
143 }
144
145 for child in children {
146 match child {
147 MoxItem::None => (),
148 nonempty_child => quote!(.child(#nonempty_child)).to_tokens(&mut contents),
149 }
150 }
151
152 quote!(mox::topo::call(|| { #name() #contents .build() })).to_tokens(tokens);
154 }
155}
156
157struct MoxAttr {
158 name: syn::Ident,
159 value: Option<syn::Expr>,
160}
161
162impl TryFrom<syn_rsx::Node> for MoxAttr {
163 type Error = syn::Error;
164
165 fn try_from(node: syn_rsx::Node) -> syn::Result<Self> {
166 match node.node_type {
167 NodeType::Element
168 | NodeType::Text
169 | NodeType::Block
170 | NodeType::Comment
171 | NodeType::Doctype
172 | NodeType::Fragment => Err(Self::node_convert_error(&node)),
173 NodeType::Attribute => {
174 Ok(MoxAttr { name: MoxAttr::validate_name(node.name.unwrap())?, value: node.value })
175 }
176 }
177 }
178}
179
180impl MoxAttr {
181 fn validate_name(name: syn_rsx::NodeName) -> syn::Result<syn::Ident> {
182 use syn::{punctuated::Pair, PathSegment};
183
184 let invalid_error = |span| syn::Error::new(span, "Invalid name for an attribute");
185
186 match name {
187 NodeName::Path(syn::ExprPath {
188 attrs,
189 qself: None,
190 path: syn::Path { leading_colon: None, mut segments },
191 }) if attrs.is_empty() && segments.len() == 1 => {
192 let pair = segments.pop();
193 match pair {
194 Some(Pair::End(PathSegment { mut ident, arguments }))
195 if arguments.is_empty() =>
196 {
197 mangle_ident(&mut ident);
198 Ok(ident)
199 }
200 _ => Err(invalid_error(segments.span())),
202 }
203 }
204 NodeName::Dash(punctuated) => {
205 Err(syn::Error::new(
207 punctuated.span(),
208 "Dash attribute name syntax isn't supported",
209 ))
210 }
211 NodeName::Colon(punctuated) => Err(syn::Error::new(
212 punctuated.span(),
213 "Colon attribute name syntax isn't supported",
214 )),
215 name => Err(invalid_error(name.span())),
216 }
217 }
218}
219
220impl ToTokens for MoxAttr {
221 fn to_tokens(&self, tokens: &mut TokenStream) {
222 let Self { name, value } = self;
223 match value {
224 Some(value) => tokens.extend(quote!(.#name(#value))),
225 None => tokens.extend(quote!(.#name(#name))),
226 };
227 }
228}
229
230struct MoxExpr {
231 expr: syn::Expr,
232}
233
234impl TryFrom<syn_rsx::Node> for MoxExpr {
235 type Error = syn::Error;
236
237 fn try_from(node: syn_rsx::Node) -> syn::Result<Self> {
238 match node.node_type {
239 NodeType::Element
240 | NodeType::Attribute
241 | NodeType::Comment
242 | NodeType::Doctype
243 | NodeType::Fragment => Err(Self::node_convert_error(&node)),
244 NodeType::Text | NodeType::Block => Ok(MoxExpr { expr: node.value.unwrap() }),
245 }
246 }
247}
248
249impl ToTokens for MoxExpr {
250 fn to_tokens(&self, tokens: &mut TokenStream) {
251 let Self { expr } = self;
252 quote!(#expr.into_child()).to_tokens(tokens);
253 }
254}
255
256trait NodeConvertError {
257 fn node_convert_error(node: &syn_rsx::Node) -> syn::Error {
258 syn::Error::new(
259 node_span(&node),
260 format_args!("Cannot convert {} to {}", node.node_type, std::any::type_name::<Self>(),),
261 )
262 }
263}
264
265impl<T> NodeConvertError for T where T: TryFrom<syn_rsx::Node> {}
266
267fn mangle_expr_path(name: &mut syn::ExprPath) {
268 for segment in name.path.segments.iter_mut() {
269 mangle_ident(&mut segment.ident);
270 }
271}
272
273fn mangle_ident(ident: &mut syn::Ident) {
274 let name = ident.to_string();
275 match name.as_str() {
276 "async" | "for" | "loop" | "type" => *ident = syn::Ident::new(&(name + "_"), ident.span()),
277 _ => (),
278 }
279}
280
281fn node_span(node: &syn_rsx::Node) -> Span {
282 node.name_span()
285 .or_else(|| node.value.as_ref().map(|value| value.span()))
286 .unwrap_or_else(Span::call_site)
287}
288
289#[cfg(test)]
290#[test]
291fn fails() {
292 fn assert_error(input: TokenStream) {
293 match syn::parse2::<MoxItem>(input) {
294 Ok(_) => unreachable!(),
295 Err(error) => println!("{}", error),
296 }
297 }
298
299 println!();
300 assert_error(quote! { <colon:tag:name /> });
301 assert_error(quote! { <{"block tag name"} /> });
302 assert_error(quote! { <some::tag colon:attribute:name=() /> });
303 assert_error(quote! { <some::tag path::attribute::name=() /> });
304 assert_error(quote! { {% "1: {}; 2: {}", var1, var2 tail } });
305 println!();
306}