1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::{quote, ToTokens};
5use syn::{
6 braced, parenthesized,
7 ext::IdentExt,
8 parse::{Parse, ParseStream},
9 parse_macro_input, token, Expr, Ident, ItemFn, LitStr, Path, Result, Token,
10};
11
12struct RsxInput {
14 root: Node,
15}
16
17impl Parse for RsxInput {
18 fn parse(input: ParseStream) -> Result<Self> {
19 Ok(RsxInput {
20 root: input.parse()?,
21 })
22 }
23}
24
25enum Node {
27 Element(Element),
28 Component(ComponentElement),
29 Text(LitStr),
30 ReactiveText(Expr),
31 RenderedNode(Expr),
32}
33
34impl Parse for Node {
35 fn parse(input: ParseStream) -> Result<Self> {
36 if input.peek(Token![<]) {
37 let fork = input.fork();
38 fork.parse::<Token![<]>()?;
39 let path: Path = fork.parse()?;
40 let first_char = path
41 .segments
42 .first()
43 .unwrap()
44 .ident
45 .to_string()
46 .chars()
47 .next()
48 .unwrap();
49
50 if first_char.is_ascii_uppercase() {
51 return Ok(Node::Component(input.parse()?));
52 } else {
53 return Ok(Node::Element(input.parse()?));
54 }
55 } else if input.peek(LitStr) {
56 Ok(Node::Text(input.parse()?))
57 } else if input.peek(token::Brace) {
58 let content;
59 braced!(content in input);
60 let expr: Expr = content.parse()?;
61 if let Expr::Paren(_) = expr {
64 Ok(Node::RenderedNode(expr))
65 } else {
66 Ok(Node::ReactiveText(expr))
67 }
68 } else {
69 Err(input.error(
70 "Expected an element (`<... />`), a string literal (`\"...\"`), or a rust expression (`{...}`)",
71 ))
72 }
73 }
74}
75
76enum AttrName {
78 Standard(Path),
79 Event(Ident),
80 Binding(Ident),
81}
82
83impl Parse for AttrName {
84 fn parse(input: ParseStream) -> Result<Self> {
85 if input.peek(token::Paren) {
86 let content;
87 parenthesized!(content in input);
88 return Ok(AttrName::Event(content.parse()?));
89 }
90
91 let fork = input.fork();
92 if let Ok(path) = fork.parse::<Path>() {
93 if let Some(ident) = path.get_ident() {
94 if ident == "bind" && fork.peek(Token![:]) {
95 input.parse::<Ident>()?; input.parse::<Token![:]>()?; return Ok(AttrName::Binding(input.parse()?));
99 }
100 }
101 }
102
103 Ok(AttrName::Standard(syn::Ident::parse_any(input)?.into()))
106 }
107}
108
109enum AttrValue {
111 Literal(LitStr),
112 Expr(Expr),
113}
114
115impl Parse for AttrValue {
116 fn parse(input: ParseStream) -> Result<Self> {
117 if input.peek(token::Brace) {
118 let content;
119 braced!(content in input);
120 Ok(AttrValue::Expr(content.parse()?))
121 } else {
122 Ok(AttrValue::Literal(input.parse()?))
123 }
124 }
125}
126
127impl ToTokens for AttrValue {
128 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
129 match self {
130 AttrValue::Literal(lit) => lit.to_tokens(tokens),
131 AttrValue::Expr(expr) => {
132 tokens.extend(quote! { &format!("{}", #expr) });
133 }
134 }
135 }
136}
137
138struct Attribute {
140 name: AttrName,
141 value: AttrValue,
142}
143
144impl Parse for Attribute {
145 fn parse(input: ParseStream) -> Result<Self> {
146 let name = input.parse()?;
147 input.parse::<Token![=]>()?;
148 let value = input.parse()?;
149 Ok(Attribute { name, value })
150 }
151}
152
153struct Element {
155 name: Ident,
156 attrs: Vec<Attribute>,
157 children: Vec<Node>,
158}
159
160impl Parse for Element {
161 fn parse(input: ParseStream) -> Result<Self> {
162 input.parse::<Token![<]>()?;
163 let name: Ident = input.parse()?;
164
165 let mut attrs = Vec::new();
166 while !input.peek(Token![>]) && !input.peek(Token![/]) {
167 attrs.push(input.parse()?);
168 }
169
170 if input.peek(Token![/]) {
171 input.parse::<Token![/]>()?;
172 input.parse::<Token![>]>()?;
173 return Ok(Element {
174 name,
175 attrs,
176 children: Vec::new(),
177 });
178 }
179 input.parse::<Token![>]>()?;
180
181 let mut children = Vec::new();
182 while !input.peek(Token![<]) || !input.peek2(Token![/]) {
183 children.push(input.parse()?);
184 }
185
186 input.parse::<Token![<]>()?;
187 input.parse::<Token![/]>()?;
188 let closing_name: Ident = input.parse()?;
189 if closing_name != name {
190 let error_message = format!(
191 "Mismatched closing tag: expected `{}`, found `{}`",
192 name, closing_name
193 );
194 return Err(input.error(error_message));
195 }
196 input.parse::<Token![>]>()?;
197
198 Ok(Element {
199 name,
200 attrs,
201 children,
202 })
203 }
204}
205
206struct ComponentElement {
208 name: Path,
209 props: Vec<Attribute>,
210 children: Vec<Node>,
211}
212
213impl Parse for ComponentElement {
214 fn parse(input: ParseStream) -> Result<Self> {
215 input.parse::<Token![<]>()?;
217 let name: Path = input.parse()?;
218
219 let mut props = Vec::new();
221 while !input.peek(Token![>]) && !input.peek(Token![/]) {
222 props.push(input.parse()?);
223 }
224
225 if input.peek(Token![/]) {
227 input.parse::<Token![/]>()?;
228 input.parse::<Token![>]>()?;
229 return Ok(ComponentElement {
230 name,
231 props,
232 children: Vec::new(),
233 });
234 }
235 input.parse::<Token![>]>()?;
236
237 let mut children = Vec::new();
239 while !input.peek(Token![<]) || !input.peek2(Token![/]) {
240 children.push(input.parse()?);
241 }
242
243 input.parse::<Token![<]>()?;
245 input.parse::<Token![/]>()?;
246 let closing_name: Path = input.parse()?;
247 if closing_name != name {
248 let error_message = format!(
249 "Mismatched closing tag: expected `{}`, found `{}`",
250 quote!(#name).to_string(),
251 quote!(#closing_name).to_string()
252 );
253 return Err(input.error(error_message));
254 }
255 input.parse::<Token![>]>()?;
256
257 Ok(ComponentElement {
258 name,
259 props,
260 children,
261 })
262 }
263}
264
265impl ToTokens for RsxInput {
266 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
267 self.root.to_tokens(tokens);
268 }
269}
270
271impl ToTokens for Node {
272 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
273 match self {
274 Node::Element(el) => el.to_tokens(tokens),
275 Node::Component(comp) => comp.to_tokens(tokens),
276 Node::Text(text) => {
277 tokens.extend(quote! {
278 fenrix_dom::create_text_node(#text).into()
279 });
280 }
281 Node::ReactiveText(expr) => {
282 tokens.extend(quote! {
283 fenrix_dom::create_reactive_text_node(move || format!("{}", #expr)).into()
284 });
285 }
286 Node::RenderedNode(expr) => {
287 tokens.extend(quote! {
289 #expr
290 });
291 }
292 }
293 }
294}
295
296impl ToTokens for Element {
297 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
298 let tag_name = self.name.to_string();
299 let children = &self.children;
300
301 let mut event_handlers = Vec::new();
302 let mut standard_attrs = Vec::new();
303 let mut bindings = Vec::new();
304
305 for attr in &self.attrs {
306 match &attr.name {
307 AttrName::Event(_) => event_handlers.push(attr),
308 AttrName::Binding(_) => bindings.push(attr),
309 AttrName::Standard(_) => standard_attrs.push(attr),
310 }
311 }
312
313 let set_attributes_code = standard_attrs.iter().map(|attr| {
314 if let AttrName::Standard(name) = &attr.name {
315 let name_str = quote!(#name).to_string();
316 let value = &attr.value;
317 quote! { element.set_attribute(#name_str, #value).unwrap(); }
318 } else {
319 quote! {}
320 }
321 });
322
323 let add_event_listeners_code = event_handlers.iter().map(|attr| {
324 if let AttrName::Event(name) = &attr.name {
325 if let AttrValue::Expr(handler) = &attr.value {
326 let event_name = name.to_string();
327 quote! {
328 let closure = ::wasm_bindgen::prelude::Closure::wrap(Box::new(#handler) as Box<dyn FnMut(_)>);
329 element.add_event_listener_with_callback(#event_name, closure.as_ref().unchecked_ref()).unwrap();
330 closure.forget();
331 }
332 } else {
333 quote! { compile_error!("Event handler must be a closure in braces."); }
334 }
335 } else {
336 quote! {}
337 }
338 });
339
340 let add_bindings_code = bindings.iter().map(|attr| {
341 if let AttrName::Binding(name) = &attr.name {
342 if name == "value" {
343 if let AttrValue::Expr(signal_expr) = &attr.value {
344 quote! {
345 let (getter, setter) = #signal_expr;
346 let _el = element.clone();
347 fenrix_core::create_effect(move || {
348 let el = _el.dyn_ref::<::web_sys::HtmlInputElement>().unwrap();
349 el.set_value(&getter());
350 });
351 let closure = ::wasm_bindgen::prelude::Closure::wrap(Box::new(move |event: ::web_sys::InputEvent| {
352 let target = event.target().unwrap();
353 let el = target.dyn_into::<::web_sys::HtmlInputElement>().unwrap();
354 setter(el.value());
355 }) as Box<dyn FnMut(_)>);
356 element.add_event_listener_with_callback("input", closure.as_ref().unchecked_ref()).unwrap();
357 closure.forget();
358 }
359 } else {
360 quote! { compile_error!("Binding value must be a signal expression."); }
361 }
362 } else {
363 quote! { compile_error!("Only `bind:value` is currently supported."); }
364 }
365 } else {
366 quote! {}
367 }
368 });
369
370 tokens.extend(quote! {
371 {
372 let element = fenrix_dom::create_element(#tag_name);
373 #(#set_attributes_code)*
374 #(#add_event_listeners_code)*
375 #(#add_bindings_code)*
376 #(
377 let child_node: web_sys::Node = #children;
378 fenrix_dom::append_child(&element, &child_node);
379 )*
380 element.into()
381 }
382 });
383 }
384}
385
386impl ToTokens for ComponentElement {
387 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
388 let name = &self.name;
389 let name_str = quote!(#name).to_string();
390
391 if name_str == "Link" {
392 let mut to_prop = None;
394 for prop in &self.props {
395 if let AttrName::Standard(prop_name) = &prop.name {
396 if quote!(#prop_name).to_string() == "to" {
397 to_prop = Some(&prop.value);
398 break;
399 }
400 }
401 }
402
403 if let Some(to_value) = to_prop {
404 let href_value_tokens = match to_value {
405 AttrValue::Literal(lit) => quote! { #lit },
406 AttrValue::Expr(expr) => quote! { #expr },
407 };
408
409 let href = quote! { format!("#{}", #href_value_tokens) };
410 let children = &self.children;
411
412 let onclick_handler = quote! {
413 move |event: ::web_sys::MouseEvent| {
414 event.prevent_default();
415 let path = format!("{}", #href_value_tokens);
416 ::web_sys::window().unwrap().location().set_hash(&path).unwrap();
417 }
418 };
419
420 tokens.extend(quote! {
421 {
422 let element = fenrix_dom::create_element("a");
423 element.set_attribute("href", &#href).unwrap();
424
425 let closure = ::wasm_bindgen::prelude::Closure::wrap(Box::new(#onclick_handler) as Box<dyn FnMut(_)>);
426 element.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap();
427 closure.forget();
428
429 #(
430 let child_node: web_sys::Node = #children;
431 fenrix_dom::append_child(&element, &child_node);
432 )*
433
434 element.into()
435 }
436 });
437 } else {
438 tokens.extend(quote! {
439 compile_error!("<Link> component requires a 'to' prop.")
440 });
441 }
442 } else {
443 tokens.extend(quote! {
445 #name()
446 });
447 }
448 }
449}
450
451#[proc_macro]
452pub fn rsx(input: TokenStream) -> TokenStream {
453 let parsed_input = parse_macro_input!(input as RsxInput);
454 let expanded = quote! {
455 {
456 #parsed_input
457 }
458 };
459 TokenStream::from(expanded)
460}
461
462mod server;
463
464#[proc_macro_attribute]
465pub fn server(attr: TokenStream, item: TokenStream) -> TokenStream {
466 server::server_macro(attr, item)
467}
468
469#[proc_macro_attribute]
470pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
471 let mut func = parse_macro_input!(item as ItemFn);
472 let original_block = func.block;
473 let new_block_tokens = quote! {
474 {
475 fenrix_core::with_component_context(|| #original_block)
476 }
477 };
478 func.block = Box::new(syn::parse2(new_block_tokens).unwrap());
479 let expanded = quote! {
480 #func
481 };
482 TokenStream::from(expanded)
483}