Skip to main content

fenrix_macros/
lib.rs

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
12/// Represents the overall RSX structure. For now, a single root node.
13struct 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
25/// Represents a node in the RSX tree.
26enum 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            // Convention: if the expression is parenthesized, it's a rendered node.
62            // Otherwise, it's reactive text.
63            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
76/// Represents an attribute key. Can be a simple identifier or a parenthesized event name.
77enum 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                    // It's a binding. Consume from the real input stream.
96                    input.parse::<Ident>()?; // consume "bind"
97                    input.parse::<Token![:]>()?; // consume ":"
98                    return Ok(AttrName::Binding(input.parse()?));
99                }
100            }
101        }
102
103        // Otherwise, it's a standard attribute. Use `parse_any` to allow keywords,
104        // and `.into()` to convert the resulting `Ident` into a `Path`.
105        Ok(AttrName::Standard(syn::Ident::parse_any(input)?.into()))
106    }
107}
108
109/// Represents an attribute value. Can be a literal string or a Rust expression in braces.
110enum 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
138/// Represents a single attribute on an element.
139struct 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
153/// Represents an HTML element like `<div id="main">...</div>`.
154struct 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
206/// Represents a component element like `<MyComponent to="/about">Click Me</MyComponent>`.
207struct 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        // Parse opening tag: `<ComponentName`
216        input.parse::<Token![<]>()?;
217        let name: Path = input.parse()?;
218
219        // Parse props
220        let mut props = Vec::new();
221        while !input.peek(Token![>]) && !input.peek(Token![/]) {
222            props.push(input.parse()?);
223        }
224
225        // Handle self-closing `/>`
226        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        // Parse children
238        let mut children = Vec::new();
239        while !input.peek(Token![<]) || !input.peek2(Token![/]) {
240            children.push(input.parse()?);
241        }
242
243        // Parse closing tag: `</ComponentName>`
244        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                // The expression is already a Node, so just pass it through.
288                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            // Special handling for the built-in <Link> component.
393            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            // TODO: Implement passing props and children to user-defined components.
444            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}