1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use syn::parse_macro_input;
5
6mod html_parser;
7
8#[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]
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 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}