mox_impl/
lib.rs

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            // TODO(#232) implement
107            | 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                // TODO support dash tag name syntax, see `https://github.com/anp/moxie/issues/233`
121                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        // this needs to be nested within other token groups, must be accumulated
138        // separately from stream
139        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        // TODO remove `topo` dependency, see `https://github.com/anp/moxie/issues/199`
153        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                    // TODO improve error handling, see `https://github.com/stoically/syn-rsx/issues/12`
201                    _ => Err(invalid_error(segments.span())),
202                }
203            }
204            NodeName::Dash(punctuated) => {
205                // TODO support dash tag name syntax, see `https://github.com/anp/moxie/issues/233`
206                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    // TODO get the span for the whole node, see `https://github.com/stoically/syn-rsx/issues/14`
283    // Prioritize name's span then value's span then call site's span.
284    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}