mogwai-html-macro 0.2.2

Declare mogwai views with RSX
Documentation
//! RSX for building mogwai DOM nodes.
use proc_macro2::Span;
use quote::quote;
use syn::Error;
use syn_rsx::{Node, NodeType};

fn cast_type_attribute(node: &Node) -> Option<proc_macro2::TokenStream> {
    let key = node.name_as_string()?;
    let expr = node.value.as_ref()?;
    match key.split(':').collect::<Vec<_>>().as_slice() {
        ["cast", "type"] => Some(quote! {#expr}),
        _ => None,
    }
}

fn xmlns_attribute(node: &Node) -> Option<proc_macro2::TokenStream> {
    let key = node.name_as_string()?;
    let expr = node.value.as_ref()?;
    if key.starts_with("xmlns") {
        Some(quote! {#expr})
    } else {
        None
    }
}

fn attribute_to_token_stream(node: Node) -> Result<proc_macro2::TokenStream, Error> {
    let span = node.name_span().unwrap_or(Span::call_site());
    if let Some(key) = node.name_as_string() {
        if let Some(expr) = node.value {
            match key.split(':').collect::<Vec<_>>().as_slice() {
                ["style", name] => Ok(quote! {
                    __mogwai_node.style(#name, #expr);
                }),
                ["on", event] => Ok(quote! {
                    __mogwai_node.on(#event, #expr);
                }),
                ["window", event] => Ok(quote! {
                    __mogwai_node.window_on(#event, #expr);
                }),
                ["document", event] => Ok(quote! {
                    __mogwai_node.document_on(#event, #expr);
                }),
                ["post", "build"] => Ok(quote! {
                    __mogwai_node.post_build(#expr);
                }),
                ["boolean", name] => Ok(quote! {
                    __mogwai_node.boolean_attribute(#name, #expr);
                }),
                ["patch", "children"] => Ok(quote! {
                    __mogwai_node.patch(#expr);
                }),
                ["cast", "type"] => Ok(quote! {}),
                [attribute_name] => Ok(quote! {
                    __mogwai_node.attribute(#attribute_name, #expr);
                }),
                keys => {
                    let attribute_name = keys.join(":");
                    Ok(quote! {
                        __mogwai_node.attribute(#attribute_name, #expr);
                    })
                }
            }
        } else {
            Ok(quote! {
                __mogwai_node.boolean_attribute(#key, true);
            })
        }
    } else {
        Err(Error::new(span, "dom attribute is missing a name"))
    }
}

fn partition_unzip<T, F>(items: Vec<T>, f: F) -> (Vec<proc_macro2::TokenStream>, Vec<Error>)
where
    F: Fn(T) -> Result<proc_macro2::TokenStream, Error>,
{
    let (tokens, errs): (Vec<Result<_, _>>, _) = items.into_iter().map(f).partition(Result::is_ok);
    let tokens = tokens
        .into_iter()
        .filter_map(Result::ok)
        .collect::<Vec<_>>();
    let errs = errs.into_iter().filter_map(Result::err).collect::<Vec<_>>();
    (tokens, errs)
}

fn combine_errors(errs: Vec<Error>) -> Option<Error> {
    errs.into_iter()
        .fold(None, |may_prev_error: Option<Error>, err| {
            if let Some(mut prev_error) = may_prev_error {
                prev_error.combine(err);
                Some(prev_error)
            } else {
                Some(err)
            }
        })
}

fn walk_node<F>(
    view_path: proc_macro2::TokenStream,
    node_fn: F,
    node: Node,
) -> Result<proc_macro2::TokenStream, Error>
where
    F: Fn(Node) -> Result<proc_macro2::TokenStream, Error>,
{
    match node.node_type {
        NodeType::Element => match node.name_as_string() {
            Some(tag) => {
                let mut type_is = quote! { web_sys::HtmlElement };
                let mut namespace = None;
                for att_node in node.attributes.iter() {
                    if let Some(cast_type) = cast_type_attribute(att_node) {
                        type_is = cast_type;
                    }
                    if let Some(ns) = xmlns_attribute(att_node) {
                        namespace = Some(ns);
                    }
                }

                let mut errs: Vec<Error> = vec![];

                let (attribute_tokens, attribute_errs) =
                    partition_unzip(node.attributes, attribute_to_token_stream);
                errs.extend(attribute_errs);

                let (child_tokens, child_errs) = partition_unzip(node.children, node_fn);
                let child_tokens = child_tokens
                    .into_iter()
                    .map(|child| quote! { __mogwai_node.with(#child); });
                errs.extend(child_errs);

                let may_error = combine_errors(errs);
                if let Some(error) = may_error {
                    Err(error)
                } else {
                    let create = if let Some(ns) = namespace {
                        quote! {#view_path::element_ns(#tag, #ns)}
                    } else {
                        quote! {#view_path::element(#tag)}
                    };
                    Ok(quote! {
                        {
                            let mut __mogwai_node = #create as #view_path<#type_is>;
                            #(#attribute_tokens)*
                            #(#child_tokens)*
                            __mogwai_node
                        }
                    })
                }
            }
            _ => Err(Error::new(Span::call_site(), "node is missing a name")),
        },
        NodeType::Text => {
            if let Some(value) = node.value {
                Ok(quote! {#view_path::from(#value)})
            } else {
                Err(Error::new(
                    Span::call_site(),
                    "dom child text node value error",
                ))
            }
        }
        NodeType::Block => {
            if let Some(value) = node.value {
                Ok(quote! {#view_path::try_from(#value).ok()})
            } else {
                Err(Error::new(
                    Span::call_site(),
                    "dom child expr node value error",
                ))
            }
        }

        _ => Err(Error::new(
            Span::call_site(),
            "attribute in unsupported position",
        )),
    }
}

fn node_to_view_token_stream(node: Node) -> Result<proc_macro2::TokenStream, Error> {
    walk_node(
        quote! { mogwai::prelude::View },
        node_to_view_token_stream,
        node,
    )
}

fn node_to_hydrateview_token_stream(node: Node) -> Result<proc_macro2::TokenStream, Error> {
    walk_node(
        quote! { mogwai_hydrator::Hydrator },
        node_to_hydrateview_token_stream,
        node,
    )
}

fn node_to_builder_token_stream(node: Node) -> Result<proc_macro2::TokenStream, Error> {
    walk_node(
        quote! { mogwai::prelude::ViewBuilder },
        node_to_builder_token_stream,
        node,
    )
}

fn walk_dom(
    input: proc_macro::TokenStream,
    f: impl Fn(Node) -> Result<proc_macro2::TokenStream, Error>,
) -> proc_macro2::TokenStream {
    match syn_rsx::parse(input) {
        Ok(parsed) => {
            let (tokens, errs) = partition_unzip(parsed, f);
            if let Some(error) = combine_errors(errs) {
                error.to_compile_error().into()
            } else {
                match tokens.len() {
                    0 => quote! { compile_error("dom/hydrate macro must not be empty") },
                    1 => {
                        let ts = &tokens[0];
                        quote! { #ts }
                    }
                    _ => quote! { vec![#(#tokens),*] },
                }
            }
        }
        Err(error) => error.to_compile_error(),
    }
}

#[proc_macro]
/// Uses an html description to construct a `View`.
///
/// ```rust
/// # extern crate mogwai;
/// use mogwai::prelude::*;
///
/// let my_div = view! {
///     <div id="main">
///         <p>"Trolls are real"</p>
///     </div>
/// };
/// ```
pub fn view(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    proc_macro::TokenStream::from(walk_dom(input, node_to_view_token_stream))
}

#[proc_macro]
/// Uses an html description to construct a `Hydrator`, which can then be converted
/// into a `View` with [`std::convert::TryFrom`].
///
/// ```rust
/// # extern crate mogwai;
/// # extern crate mogwai_hydrator;
/// use mogwai::prelude::*;
/// use mogwai_hydrator::Hydrator;
///
/// let my_div = hydrate! {
///     <div id="main">
///         <p>"Trolls are real"</p>
///     </div>
/// };
/// ```
pub fn hydrate(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    proc_macro::TokenStream::from(walk_dom(input, node_to_hydrateview_token_stream))
}

#[proc_macro]
/// Uses an html description to construct a `ViewBuilder`.
///
/// ```rust
/// # extern crate mogwai;
/// use mogwai::prelude::*;
///
/// let my_div = builder! {
///     <div id="main">
///         <p>"Trolls are real"</p>
///     </div>
/// };
/// ```
pub fn builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    proc_macro::TokenStream::from(walk_dom(input, node_to_builder_token_stream))
}

#[proc_macro]
pub fn target_arch_is_wasm32(_: proc_macro::TokenStream) -> proc_macro::TokenStream {
    proc_macro::TokenStream::from(quote! {
        cfg!(target_arch = "wasm32")
    })
}

#[cfg(test)]
mod ssr_tests {
    use std::str::FromStr;

    #[test]
    fn can_parse_rust_closure() {
        let expr: syn::Expr = syn::parse_str(r#"|i:i32| format!("{}", i)"#).unwrap();
        match expr {
            syn::Expr::Closure(_) => {}
            _ => panic!("wrong expr parse, expected closure"),
        }
    }

    #[test]
    fn can_token_stream_from_string() {
        let _ts = proc_macro2::TokenStream::from_str(r#"|i:i32| format!("{}", i)"#).unwrap();
    }

    #[test]
    fn can_parse_from_token_stream() {
        let _ts = proc_macro2::TokenStream::from_str(r#"<div class="any_class" />"#).unwrap();
    }
}