workers-rsx-impl 0.1.0

Proc macros for workers-rsx
Documentation
extern crate proc_macro;

mod child;
mod children;
mod css;
mod element;
mod element_attribute;
mod element_attributes;
mod function_component;
mod tags;

use element::Element;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use quote::quote;
use syn::parse_macro_input;

/// Render a JSX-like template to an HTML string.
/// Automatically extracts Tailwind class names and injects generated CSS as a `<style>` tag
/// inside `<head>` (or prepended if no `<head>` is found).
#[proc_macro]
#[proc_macro_error]
pub fn html(input: TokenStream) -> TokenStream {
    let mut el = parse_macro_input!(input as Element);
    inject_tailwind(&mut el);
    let result = quote! { ::workers_rsx::Render::render(#el) };
    TokenStream::from(result)
}

/// Generate a renderable component tree without rendering it
#[proc_macro]
#[proc_macro_error]
pub fn rsx(input: TokenStream) -> TokenStream {
    let el = parse_macro_input!(input as Element);
    let result = quote! { #el };
    TokenStream::from(result)
}

/// Render a JSX-like template and return it as a `worker::Response` with HTML content type.
/// Automatically extracts Tailwind class names and injects generated CSS as a `<style>` tag
/// inside `<head>` (or prepended if no `<head>` is found).
#[proc_macro]
#[proc_macro_error]
pub fn view(input: TokenStream) -> TokenStream {
    let mut el = parse_macro_input!(input as Element);
    inject_tailwind(&mut el);
    let result = quote! {
        ::worker::Response::from_html(::workers_rsx::Render::render(#el))
    };
    TokenStream::from(result)
}

/// Parse CSS syntax at compile time and return it as a `&'static str`
///
/// ```ignore
/// let styles = css! {
///     .container {
///         max-width: 600px;
///         margin: 0 auto;
///     }
///     .title {
///         font-size: 2rem;
///         color: #333;
///     }
/// };
/// ```
#[proc_macro]
#[proc_macro_error]
pub fn css(input: TokenStream) -> TokenStream {
    let input2 = proc_macro2::TokenStream::from(input);
    let css_string = css::tokens_to_css(input2);
    let result = quote! { #css_string };
    TokenStream::from(result)
}

/// Define a function component as a struct that implements `Render`
#[proc_macro_attribute]
#[proc_macro_error]
pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let f = parse_macro_input!(item as syn::ItemFn);
    function_component::create_function_component(f, None)
}

/// Define a page component: a `#[component]` that also implements `From<State>`.
///
/// Use `#[page(StateType)]` to specify the state type. The component must have
/// a `state` field of that type as its first parameter.
///
/// ```ignore
/// #[page(PageState)]
/// fn TodosPage(state: PageState) {
///     rsx! { <div>{state.title}</div> }
/// }
/// // generates: struct TodosPage, impl Render, impl From<PageState> for TodosPage
/// ```
#[proc_macro_attribute]
#[proc_macro_error]
pub fn page(attr: TokenStream, item: TokenStream) -> TokenStream {
    let state_ident = parse_macro_input!(attr as syn::Ident);
    let f = parse_macro_input!(item as syn::ItemFn);
    function_component::create_function_component(f, Some(state_ident))
}

/// Collect unique class names from an Element tree (compile-time extraction).
fn collect_unique_classes(el: &Element) -> Vec<String> {
    let all = el.collect_class_names();
    let mut seen = std::collections::HashSet::new();
    all.into_iter()
        .filter(|c| seen.insert(c.clone()))
        .collect()
}

/// Extract Tailwind class names and inject a `TailwindStyle` child into the `<head>` element.
/// If no `<head>` is found, the style is prepended as the first child of the root element.
fn inject_tailwind(el: &mut Element) {
    let class_names = collect_unique_classes(el);
    if class_names.is_empty() {
        return;
    }
    if !el.inject_tailwind_style(class_names.clone()) {
        // No <head> found — prepend as first child of root
        el.prepend_child(child::Child::TailwindStyle(class_names));
    }
}

/// Derive `type_tag()` and `json()` methods for a `#[serde(tag = "...")]` enum.
///
/// `type_tag()` returns the variant name as a `&'static str`.
/// `json()` returns `serde_json::to_string(self).unwrap()`.
///
/// ```ignore
/// #[derive(Serialize, Deserialize, ActionJson)]
/// #[serde(tag = "type")]
/// enum AppAction {
///     AddTodo { name: String },
///     ToggleTodo { id: String },
/// }
///
/// // generates:
/// // AppAction::AddTodo { .. }.type_tag() => "AddTodo"
/// // AppAction::AddTodo { name: "foo".into() }.json() => r#{"type":"AddTodo","name":"foo"}#
/// ```
#[proc_macro_derive(ActionJson)]
pub fn derive_action(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as syn::DeriveInput);
    let name = &input.ident;

    let variants = match &input.data {
        syn::Data::Enum(data) => &data.variants,
        _ => {
            return syn::Error::new_spanned(&input, "ActionJson can only be derived for enums")
                .to_compile_error()
                .into();
        }
    };

    let arms: Vec<_> = variants
        .iter()
        .map(|v| {
            let ident = &v.ident;
            let tag = ident.to_string();
            match &v.fields {
                syn::Fields::Unit => quote! { #name::#ident => #tag },
                _ => quote! { #name::#ident { .. } => #tag },
            }
        })
        .collect();

    let expanded = quote! {
        impl #name {
            fn type_tag(&self) -> &'static str {
                match self {
                    #(#arms),*
                }
            }

            fn json(&self) -> String {
                ::workers_rsx::serde_json::to_string(self).unwrap()
            }
        }

        impl ::std::fmt::Display for #name {
            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
                f.write_str(&::workers_rsx::serde_json::to_string(self).unwrap())
            }
        }

        impl<'a> ::std::convert::From<#name> for ::std::borrow::Cow<'a, str> {
            fn from(action: #name) -> Self {
                ::std::borrow::Cow::Owned(action.to_string())
            }
        }
    };

    TokenStream::from(expanded)
}