Skip to main content

workers_rsx_impl/
lib.rs

1extern crate proc_macro;
2
3mod child;
4mod children;
5mod css;
6mod element;
7mod element_attribute;
8mod element_attributes;
9mod function_component;
10mod tags;
11
12use element::Element;
13use proc_macro::TokenStream;
14use proc_macro_error::proc_macro_error;
15use quote::quote;
16use syn::parse_macro_input;
17
18/// Render a JSX-like template to an HTML string.
19/// Automatically extracts Tailwind class names and injects generated CSS as a `<style>` tag
20/// inside `<head>` (or prepended if no `<head>` is found).
21#[proc_macro]
22#[proc_macro_error]
23pub fn html(input: TokenStream) -> TokenStream {
24    let mut el = parse_macro_input!(input as Element);
25    inject_tailwind(&mut el);
26    let result = quote! { ::workers_rsx::Render::render(#el) };
27    TokenStream::from(result)
28}
29
30/// Generate a renderable component tree without rendering it
31#[proc_macro]
32#[proc_macro_error]
33pub fn rsx(input: TokenStream) -> TokenStream {
34    let el = parse_macro_input!(input as Element);
35    let result = quote! { #el };
36    TokenStream::from(result)
37}
38
39/// Render a JSX-like template and return it as a `worker::Response` with HTML content type.
40/// Automatically extracts Tailwind class names and injects generated CSS as a `<style>` tag
41/// inside `<head>` (or prepended if no `<head>` is found).
42#[proc_macro]
43#[proc_macro_error]
44pub fn view(input: TokenStream) -> TokenStream {
45    let mut el = parse_macro_input!(input as Element);
46    inject_tailwind(&mut el);
47    let result = quote! {
48        ::worker::Response::from_html(::workers_rsx::Render::render(#el))
49    };
50    TokenStream::from(result)
51}
52
53/// Parse CSS syntax at compile time and return it as a `&'static str`
54///
55/// ```ignore
56/// let styles = css! {
57///     .container {
58///         max-width: 600px;
59///         margin: 0 auto;
60///     }
61///     .title {
62///         font-size: 2rem;
63///         color: #333;
64///     }
65/// };
66/// ```
67#[proc_macro]
68#[proc_macro_error]
69pub fn css(input: TokenStream) -> TokenStream {
70    let input2 = proc_macro2::TokenStream::from(input);
71    let css_string = css::tokens_to_css(input2);
72    let result = quote! { #css_string };
73    TokenStream::from(result)
74}
75
76/// Define a function component as a struct that implements `Render`
77#[proc_macro_attribute]
78#[proc_macro_error]
79pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
80    let f = parse_macro_input!(item as syn::ItemFn);
81    function_component::create_function_component(f, None)
82}
83
84/// Define a page component: a `#[component]` that also implements `From<State>`.
85///
86/// Use `#[page(StateType)]` to specify the state type. The component must have
87/// a `state` field of that type as its first parameter.
88///
89/// ```ignore
90/// #[page(PageState)]
91/// fn TodosPage(state: PageState) {
92///     rsx! { <div>{state.title}</div> }
93/// }
94/// // generates: struct TodosPage, impl Render, impl From<PageState> for TodosPage
95/// ```
96#[proc_macro_attribute]
97#[proc_macro_error]
98pub fn page(attr: TokenStream, item: TokenStream) -> TokenStream {
99    let state_ident = parse_macro_input!(attr as syn::Ident);
100    let f = parse_macro_input!(item as syn::ItemFn);
101    function_component::create_function_component(f, Some(state_ident))
102}
103
104/// Collect unique class names from an Element tree (compile-time extraction).
105fn collect_unique_classes(el: &Element) -> Vec<String> {
106    let all = el.collect_class_names();
107    let mut seen = std::collections::HashSet::new();
108    all.into_iter()
109        .filter(|c| seen.insert(c.clone()))
110        .collect()
111}
112
113/// Extract Tailwind class names and inject a `TailwindStyle` child into the `<head>` element.
114/// If no `<head>` is found, the style is prepended as the first child of the root element.
115fn inject_tailwind(el: &mut Element) {
116    let class_names = collect_unique_classes(el);
117    if class_names.is_empty() {
118        return;
119    }
120    if !el.inject_tailwind_style(class_names.clone()) {
121        // No <head> found — prepend as first child of root
122        el.prepend_child(child::Child::TailwindStyle(class_names));
123    }
124}
125
126/// Derive `type_tag()` and `json()` methods for a `#[serde(tag = "...")]` enum.
127///
128/// `type_tag()` returns the variant name as a `&'static str`.
129/// `json()` returns `serde_json::to_string(self).unwrap()`.
130///
131/// ```ignore
132/// #[derive(Serialize, Deserialize, ActionJson)]
133/// #[serde(tag = "type")]
134/// enum AppAction {
135///     AddTodo { name: String },
136///     ToggleTodo { id: String },
137/// }
138///
139/// // generates:
140/// // AppAction::AddTodo { .. }.type_tag() => "AddTodo"
141/// // AppAction::AddTodo { name: "foo".into() }.json() => r#{"type":"AddTodo","name":"foo"}#
142/// ```
143#[proc_macro_derive(ActionJson)]
144pub fn derive_action(input: TokenStream) -> TokenStream {
145    let input = parse_macro_input!(input as syn::DeriveInput);
146    let name = &input.ident;
147
148    let variants = match &input.data {
149        syn::Data::Enum(data) => &data.variants,
150        _ => {
151            return syn::Error::new_spanned(&input, "ActionJson can only be derived for enums")
152                .to_compile_error()
153                .into();
154        }
155    };
156
157    let arms: Vec<_> = variants
158        .iter()
159        .map(|v| {
160            let ident = &v.ident;
161            let tag = ident.to_string();
162            match &v.fields {
163                syn::Fields::Unit => quote! { #name::#ident => #tag },
164                _ => quote! { #name::#ident { .. } => #tag },
165            }
166        })
167        .collect();
168
169    let expanded = quote! {
170        impl #name {
171            fn type_tag(&self) -> &'static str {
172                match self {
173                    #(#arms),*
174                }
175            }
176
177            fn json(&self) -> String {
178                ::workers_rsx::serde_json::to_string(self).unwrap()
179            }
180        }
181
182        impl ::std::fmt::Display for #name {
183            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
184                f.write_str(&::workers_rsx::serde_json::to_string(self).unwrap())
185            }
186        }
187
188        impl<'a> ::std::convert::From<#name> for ::std::borrow::Cow<'a, str> {
189            fn from(action: #name) -> Self {
190                ::std::borrow::Cow::Owned(action.to_string())
191            }
192        }
193    };
194
195    TokenStream::from(expanded)
196}