Skip to main content

rullst_macros/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use syn::parse_macro_input;
5
6mod html_parser;
7
8/// A macro for writing HTML inline in Rust.
9/// It compiles down to highly optimized string concatenations at compile time,
10/// and automatically escapes dynamic variables to prevent XSS.
11///
12/// # Example
13/// ```rust,ignore
14/// let name = "Mundo";
15/// let page = html! {
16///     <div class="container">
17///         <h1>"Olá, " {name} "!"</h1>
18///     </div>
19/// };
20/// ```
21#[proc_macro]
22pub fn html(input: TokenStream) -> TokenStream {
23    let node = parse_macro_input!(input as html_parser::HtmlNode);
24    let expanded = node.to_tokens();
25    expanded.into()
26}
27
28/// Proc macro attribute to define a Wasm Island client component.
29///
30/// It compiles dual versions depending on compilation targets:
31/// - On native server compiles, it wraps the component's HTML output in a `<div data-island="..." data-props="...">`
32/// - On wasm32-unknown-unknown compiles, it generates structural props parsing and registers a hydration function
33#[proc_macro_attribute]
34#[allow(clippy::collapsible_if)]
35pub fn client_component(_attr: TokenStream, item: TokenStream) -> TokenStream {
36    let input_fn = parse_macro_input!(item as syn::ItemFn);
37    let vis = &input_fn.vis;
38    let sig = &input_fn.sig;
39    let name = &sig.ident;
40    let body = &input_fn.block;
41
42    // Extract argument names and types
43    let mut arg_names = Vec::new();
44    let mut arg_types = Vec::new();
45
46    for arg in &sig.inputs {
47        if let syn::FnArg::Typed(pat_type) = arg {
48            if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
49                arg_names.push(&pat_ident.ident);
50                arg_types.push(&pat_type.ty);
51            }
52        }
53    }
54
55    let props_struct_name =
56        syn::Ident::new(&format!("{}_Props", name), proc_macro2::Span::call_site());
57
58    let hydrate_fn_name =
59        syn::Ident::new(&format!("hydrate_{}", name), proc_macro2::Span::call_site());
60
61    let expanded = quote::quote! {
62        #[cfg(not(target_arch = "wasm32"))]
63        #vis fn #name(#(#arg_names: #arg_types),*) -> String {
64            let inner_html = {
65                #body
66            };
67
68            let props_json = serde_json::json!({
69                #(stringify!(#arg_names): #arg_names),*
70            }).to_string();
71
72            let escaped_props = rullst::html::escape_str(&props_json);
73
74            format!(
75                "<div data-island=\"{}\" data-props=\"{}\">{}</div>",
76                stringify!(#name),
77                escaped_props,
78                inner_html
79            )
80        }
81
82        #[cfg(target_arch = "wasm32")]
83        #vis fn #name(#(#arg_names: #arg_types),*) -> String {
84            let Some(element) = web_sys::window()
85                .and_then(|w| w.document())
86                .and_then(|d| d.create_element("div").ok())
87            else {
88                return String::new();
89            };
90            let _ = {
91                #body
92            };
93            String::new()
94        }
95
96        #[cfg(target_arch = "wasm32")]
97        #[derive(serde::Deserialize)]
98        #[allow(non_camel_case_types)]
99        struct #props_struct_name {
100            #(#arg_names: #arg_types),*
101        }
102
103        #[cfg(target_arch = "wasm32")]
104        #[wasm_bindgen::prelude::wasm_bindgen]
105        #[allow(non_snake_case)]
106        pub fn #hydrate_fn_name(element: web_sys::Element, props_json: &str) {
107            let props: #props_struct_name = match serde_json::from_str(props_json) {
108                Ok(p) => p,
109                Err(_) => return,
110            };
111
112            #(let #arg_names = props.#arg_names;)*
113            let element = element;
114
115            let _ = {
116                #body
117            };
118        }
119    };
120
121    expanded.into()
122}