mogwai_html_macro/
lib.rs

1//! RSX for building mogwai DOM nodes.
2use std::convert::TryFrom;
3
4use quote::quote;
5use syn::Error;
6
7mod tokens;
8use tokens::{AttributeToken, ViewToken};
9
10fn partition_unzip<S, T, F>(items: impl Iterator<Item = S>, f: F) -> (Vec<T>, Vec<Error>)
11where
12    F: Fn(S) -> Result<T, Error>,
13{
14    let (tokens, errs): (Vec<Result<_, _>>, _) = items.map(f).partition(Result::is_ok);
15    let tokens = tokens
16        .into_iter()
17        .filter_map(Result::ok)
18        .collect::<Vec<_>>();
19    let errs = errs.into_iter().filter_map(Result::err).collect::<Vec<_>>();
20    (tokens, errs)
21}
22
23fn combine_errors(errs: Vec<Error>) -> Option<Error> {
24    errs.into_iter()
25        .fold(None, |may_prev_error: Option<Error>, err| {
26            if let Some(mut prev_error) = may_prev_error {
27                prev_error.combine(err);
28                Some(prev_error)
29            } else {
30                Some(err)
31            }
32        })
33}
34
35fn node_to_builder_token_stream(view_token: &ViewToken) -> Result<proc_macro2::TokenStream, Error> {
36    let view_path = quote! { mogwai::builder::ViewBuilder };
37    match view_token {
38        ViewToken::Element {
39            name,
40            name_span: _,
41            attributes,
42            children,
43        } => {
44            let may_type = attributes.iter().find_map(|att| match att {
45                AttributeToken::CastType(expr) => {
46                    Some(quote! { as mogwai::builder::ViewBuilder<#expr> })
47                }
48                _ => None,
49            });
50
51            let type_is = may_type
52                .unwrap_or_else(|| quote! {as mogwai::builder::ViewBuilder<mogwai::view::Dom>});
53
54            let mut errs = vec![];
55            let (attribute_tokens, attribute_errs) =
56                partition_unzip(attributes.iter(), AttributeToken::try_builder_token_stream);
57            errs.extend(attribute_errs);
58
59            let (child_tokens, child_errs) =
60                partition_unzip(children.iter(), node_to_builder_token_stream);
61            let child_tokens = child_tokens.into_iter().map(|child| {
62                quote! {
63                        .append(#child)
64                }
65            });
66            errs.extend(child_errs);
67
68            let may_error = combine_errors(errs);
69            if let Some(error) = may_error {
70                Err(error)
71            } else {
72                let create = quote! {#view_path::element(#name)};
73                Ok(quote! {
74                    #create
75                        #(#attribute_tokens)*
76                        #(#child_tokens)*
77                        #type_is
78                })
79            }
80        }
81        ViewToken::Text(expr) => Ok(quote! {mogwai::builder::ViewBuilder::text(#expr)}),
82        ViewToken::Block(expr) => Ok(quote! {
83            #[allow(unused_braces)]
84            #expr
85        }),
86    }
87}
88
89#[proc_macro]
90/// Uses an html description to construct a `ViewBuilder`.
91///
92/// ```rust
93/// extern crate mogwai;
94///
95/// let my_div = mogwai::macros::builder! {
96///     <div cast:type=mogwai::view::Dom id="main">
97///         <p>"Trolls are real"</p>
98///     </div>
99/// };
100/// ```
101pub fn builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
102    let tokens = match syn_rsx::parse(input) {
103        Ok(parsed) => {
104            let (view_tokens, errs) = partition_unzip(parsed.into_iter(), ViewToken::try_from);
105            if let Some(error) = combine_errors(errs) {
106                return error.to_compile_error().into();
107            }
108            let (tokens, errs) = partition_unzip(view_tokens.iter(), node_to_builder_token_stream);
109            if let Some(error) = combine_errors(errs) {
110                return error.to_compile_error().into();
111            }
112
113            match tokens.len() {
114                0 => quote! { compile_error("dom/hydrate macro must not be empty") },
115                1 => {
116                    let ts = &tokens[0];
117                    quote! { #ts }
118                }
119                _ => quote! { vec![#(#tokens),*] },
120            }
121        }
122        Err(error) => error.to_compile_error(),
123    };
124
125    proc_macro::TokenStream::from(tokens)
126}
127
128#[proc_macro]
129/// Uses an html description to construct a `View`.
130///
131/// This is the same as the following:
132/// ```rust
133/// extern crate mogwai;
134///
135/// let my_div = mogwai::macros::view! {
136///     <div cast:type=mogwai::view::Dom id="main">
137///         <p>"Trolls are real"</p>
138///     </div>
139/// };
140/// ```
141pub fn view(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
142    let builder: proc_macro2::TokenStream = builder(input).into();
143    let token = quote! {{
144        use std::convert::TryFrom;
145        mogwai::view::View::try_from(#builder).unwrap()
146    }};
147    proc_macro::TokenStream::from(token)
148}
149
150#[proc_macro]
151pub fn target_arch_is_wasm32(_: proc_macro::TokenStream) -> proc_macro::TokenStream {
152    proc_macro::TokenStream::from(quote! {
153        cfg!(target_arch = "wasm32")
154    })
155}
156
157#[cfg(test)]
158mod ssr_tests {
159    use std::str::FromStr;
160
161    #[test]
162    fn can_parse_rust_closure() {
163        let expr: syn::Expr = syn::parse_str(r#"|i:i32| format!("{}", i)"#).unwrap();
164        match expr {
165            syn::Expr::Closure(_) => {}
166            _ => panic!("wrong expr parse, expected closure"),
167        }
168    }
169
170    #[test]
171    fn can_token_stream_from_string() {
172        let _ts = proc_macro2::TokenStream::from_str(r#"|i:i32| format!("{}", i)"#).unwrap();
173    }
174
175    #[test]
176    fn can_parse_from_token_stream() {
177        let _ts = proc_macro2::TokenStream::from_str(r#"<div class="any_class" />"#).unwrap();
178    }
179}