Skip to main content

bubba_macros/
lib.rs

1//! # `view!` Procedural Macro
2//!
3//! Transforms declarative JSX-like UI syntax into Rust [`Element`] builder calls.
4//!
5//! ## Input (what you write)
6//! ```rust,ignore
7//! view! {
8//!     <h1 class="title">"Welcome to Bubba"</h1>
9//!     <button class="primary-btn" onclick=alert("Tapped!")>
10//!         "Tap me"
11//!     </button>
12//!     <input class="text-input" oninput=log("Typing...") />
13//! }
14//! ```
15//!
16//! ## Output (what it expands to)
17//! ```rust,ignore
18//! {
19//!     use bubba_core::ui::Element;
20//!     use bubba_core::events::EventHandler;
21//!
22//!     let mut __root = Element::div();
23//!     __root = __root.child(
24//!         Element::h1()
25//!             .class("title")
26//!             .text("Welcome to Bubba")
27//!     );
28//!     __root = __root.child(
29//!         Element::button()
30//!             .class("primary-btn")
31//!             .text("Tap me")
32//!             .on(EventHandler::onclick(|_| { alert("Tapped!") }))
33//!     );
34//!     __root = __root.child(
35//!         Element::input()
36//!             .class("text-input")
37//!             .on(EventHandler::oninput(|_| { log("Typing...") }))
38//!     );
39//!     bubba_core::ui::Screen::new(__root)
40//! }
41//! ```
42
43use proc_macro::TokenStream;
44use proc_macro2::{Span, TokenStream as TokenStream2};
45use quote::{quote, quote_spanned};
46use syn::{
47    parse::{Parse, ParseStream},
48    parse_macro_input,
49    spanned::Spanned,
50    Expr, Ident, LitStr, Result, Token,
51};
52
53// ── Public macro entry point ──────────────────────────────────────────────────
54
55/// Declare a screen's UI declaratively using JSX-like syntax.
56///
57/// # Supported Tags
58/// `<h1>`, `<h2>`, `<h3>`, `<p>`, `<button>`, `<img>`, `<input>`,
59/// `<div>`, `<span>`, `<a>`
60///
61/// # Supported Attributes
62/// - `class="..."` — CSS class name(s)
63/// - `src="..."`, `alt="..."`, `placeholder="..."`, `href="..."` — generic attrs
64/// - `onclick=expr` — tap/click handler
65/// - `oninput=expr` — input change handler  
66/// - `onkeypress=expr` — key press handler
67/// - `onfocus=expr` — focus handler
68/// - `onblur=expr` — blur handler
69///
70/// # Built-in Event Expressions
71/// - `alert("message")` — show native alert
72/// - `log("message")` — log to console
73/// - `navigate(ScreenName)` — navigate to a screen
74#[proc_macro]
75pub fn view(input: TokenStream) -> TokenStream {
76    let nodes = parse_macro_input!(input as NodeList);
77    let expanded = codegen_screen(nodes);
78    TokenStream::from(expanded)
79}
80
81// ── AST types ─────────────────────────────────────────────────────────────────
82
83/// A list of top-level nodes inside `view! { ... }`.
84struct NodeList {
85    nodes: Vec<Node>,
86}
87
88impl Parse for NodeList {
89    fn parse(input: ParseStream) -> Result<Self> {
90        let mut nodes = Vec::new();
91        while !input.is_empty() {
92            nodes.push(input.parse::<Node>()?);
93        }
94        Ok(NodeList { nodes })
95    }
96}
97
98/// A single UI node — either a tag or a text literal.
99enum Node {
100    Element(ParsedElement),
101    Text(LitStr),
102}
103
104impl Parse for Node {
105    fn parse(input: ParseStream) -> Result<Self> {
106        if input.peek(LitStr) {
107            Ok(Node::Text(input.parse()?))
108        } else {
109            Ok(Node::Element(input.parse()?))
110        }
111    }
112}
113
114/// A parsed `<tag attr=val ...> children </tag>` or `<tag ... />`.
115struct ParsedElement {
116    span: Span,
117    tag: Ident,
118    attrs: Vec<ParsedAttr>,
119    children: Vec<Node>,
120}
121
122impl Parse for ParsedElement {
123    fn parse(input: ParseStream) -> Result<Self> {
124        // `<`
125        let lt: Token![<] = input.parse().map_err(|e| {
126            syn::Error::new(e.span(), "Expected `<` to open a UI element.\n\nTip: every element starts with `<`, like `<button>` or `<h1>`.")
127        })?;
128        let span = lt.span();
129
130        // tag name
131        let tag: Ident = input.parse().map_err(|e| {
132            syn::Error::new(e.span(), "Expected a tag name after `<`.\n\nSupported tags: h1, h2, h3, p, button, img, input, div, span, a")
133        })?;
134
135        // attributes
136        let mut attrs = Vec::new();
137        while !input.peek(Token![>]) && !input.peek(Token![/]) {
138            attrs.push(input.parse::<ParsedAttr>()?);
139        }
140
141        // self-closing `/>` → return immediately; open `>` → parse children
142        if input.peek(Token![/]) {
143            input.parse::<Token![/]>()?;
144            input.parse::<Token![>]>()?;
145            return Ok(ParsedElement { span, tag, attrs, children: vec![] });
146        }
147        input.parse::<Token![>]>()?;
148
149        // children
150        let mut children = Vec::new();
151        loop {
152            // closing tag `</tag>`
153            if input.peek(Token![<]) && input.peek2(Token![/]) {
154                input.parse::<Token![<]>()?;
155                input.parse::<Token![/]>()?;
156                let closing_tag: Ident = input.parse().map_err(|e| {
157                    syn::Error::new(e.span(), "Expected closing tag name.")
158                })?;
159                input.parse::<Token![>]>()?;
160
161                if closing_tag != tag {
162                    return Err(syn::Error::new(
163                        closing_tag.span(),
164                        format!(
165                            "Mismatched tags: opened `<{}>` but closed with `</{}>`.\n\nTip: every opening tag needs a matching closing tag.",
166                            tag, closing_tag
167                        ),
168                    ));
169                }
170                break;
171            }
172            if input.is_empty() {
173                return Err(syn::Error::new(
174                    span,
175                    format!("Unclosed `<{}>` — missing `</{}>`.\n\nTip: add `</{}>` after the children.", tag, tag, tag),
176                ));
177            }
178            children.push(input.parse::<Node>()?);
179        }
180
181        Ok(ParsedElement { span, tag, attrs, children })
182    }
183}
184
185/// A single attribute: `class="..."`, `onclick=expr`, `src="..."`, etc.
186struct ParsedAttr {
187    name: Ident,
188    value: AttrValue,
189}
190
191/// The value side of an attribute.
192enum AttrValue {
193    /// A string literal: `class="title"`
194    Str(LitStr),
195    /// A Rust expression: `onclick=alert("hi")` or `onclick=navigate(Profile)`
196    Expr(Expr),
197}
198
199impl Parse for ParsedAttr {
200    fn parse(input: ParseStream) -> Result<Self> {
201        let name: Ident = input.parse().map_err(|e| {
202            syn::Error::new(e.span(), "Expected an attribute name (e.g. `class`, `onclick`, `src`).")
203        })?;
204        input.parse::<Token![=]>().map_err(|e| {
205            syn::Error::new(e.span(), format!("Attribute `{}` needs a value: `{}=\"...\"` or `{}=expr`.", name, name, name))
206        })?;
207
208        let value = if input.peek(LitStr) {
209            AttrValue::Str(input.parse()?)
210        } else {
211            // Parse a call expression like `alert("msg")`, `navigate(Screen)`,
212            // `log("x")`, or a closure `|e| { ... }`.
213            //
214            // We deliberately do NOT call `input.parse::<Expr>()` because that
215            // would greedily consume past the closing `>` into the tag's children.
216            // Instead we parse just the function name / path, then optionally
217            // a parenthesised argument list or a closure body.
218            let expr = parse_event_expr(input).map_err(|e| {
219                syn::Error::new(
220                    e.span(),
221                    format!(
222                        "Could not parse value for `{}`.\n\nExamples:\n  {}=\"some-class\"\n  {}=alert(\"message\")\n  {}=navigate(ScreenName)",
223                        name, name, name, name
224                    ),
225                )
226            })?;
227            AttrValue::Expr(expr)
228        };
229
230        Ok(ParsedAttr { name, value })
231    }
232}
233
234/// Parse an event-handler expression that must NOT consume past `>` or `/>`.
235///
236/// Accepted forms:
237///   `ident(args...)`          — function call: alert("hi"), navigate(Home)
238///   `|pat| expr`              — closure
239///   `|pat| { block }`         — closure with block
240///   `ident`                   — bare function reference
241fn parse_event_expr(input: ParseStream) -> Result<Expr> {
242    // Closure: |pat| ...
243    if input.peek(Token![|]) {
244        return input.parse::<Expr>();
245    }
246
247    // Parse a path (possibly multi-segment: foo::bar)
248    let path: syn::ExprPath = input.parse()?;
249
250    // If followed by `(`, parse the argument list
251    if input.peek(syn::token::Paren) {
252        let args_content;
253        let paren = syn::parenthesized!(args_content in input);
254        let args: syn::punctuated::Punctuated<Expr, Token![,]> =
255            args_content.parse_terminated(Expr::parse, Token![,])?;
256
257        Ok(Expr::Call(syn::ExprCall {
258            attrs: vec![],
259            func: Box::new(Expr::Path(path)),
260            paren_token: paren,
261            args,
262        }))
263    } else {
264        // Bare path / function reference
265        Ok(Expr::Path(path))
266    }
267}
268
269// ── Code generation ───────────────────────────────────────────────────────────
270
271fn codegen_screen(nodes: NodeList) -> TokenStream2 {
272    let element_stmts: Vec<TokenStream2> = nodes.nodes.iter().map(codegen_node_as_child).collect();
273    quote! {
274        {
275            let mut __bubba_root = ::bubba_core::ui::Element::div();
276            #(#element_stmts)*
277            ::bubba_core::ui::Screen::new(__bubba_root)
278        }
279    }
280}
281
282fn codegen_node_as_child(node: &Node) -> TokenStream2 {
283    match node {
284        Node::Text(lit) => {
285            quote! {
286                __bubba_root = __bubba_root.child(
287                    ::bubba_core::ui::Element::span().text(#lit)
288                );
289            }
290        }
291        Node::Element(el) => {
292            let el_expr = codegen_element(el);
293            quote! {
294                __bubba_root = __bubba_root.child(#el_expr);
295            }
296        }
297    }
298}
299
300fn codegen_element(el: &ParsedElement) -> TokenStream2 {
301    let tag = &el.tag;
302    let tag_str = tag.to_string();
303    let span = el.span;
304
305    // Start with the constructor
306    let mut builder = quote_spanned! { span =>
307        ::bubba_core::ui::Element::new(#tag_str)
308    };
309
310    // Process attributes
311    for attr in &el.attrs {
312        let attr_name = attr.name.to_string();
313        match &attr.value {
314            AttrValue::Str(s) => {
315                match attr_name.as_str() {
316                    "class" => {
317                        builder = quote! { #builder.class(#s) };
318                    }
319                    _ => {
320                        builder = quote! { #builder.attr(#attr_name, #s) };
321                    }
322                }
323            }
324            AttrValue::Expr(expr) => {
325                let event_name = match attr_name.as_str() {
326                    "onclick"    => Some("click"),
327                    "oninput"    => Some("input"),
328                    "onkeypress" => Some("keypress"),
329                    "onfocus"    => Some("focus"),
330                    "onblur"     => Some("blur"),
331                    "onchange"   => Some("change"),
332                    other => {
333                        // Unknown event — emit a compile_error pointing at the attribute
334                        let msg = format!(
335                            "Unknown event attribute `{}`. Did you mean `onclick`, `oninput`, or `onkeypress`?",
336                            other
337                        );
338                        builder = quote! {
339                            #builder
340                            compile_error!(#msg)
341                        };
342                        None
343                    }
344                };
345
346                if let Some(ev) = event_name {
347                    let handler_expr = codegen_event_expr(expr, ev);
348                    builder = quote! { #builder.on(#handler_expr) };
349                }
350            }
351        }
352    }
353
354    // Process children
355    for child in &el.children {
356        match child {
357            Node::Text(lit) => {
358                builder = quote! { #builder.text(#lit) };
359            }
360            Node::Element(child_el) => {
361                let child_expr = codegen_element(child_el);
362                builder = quote! { #builder.child(#child_expr) };
363            }
364        }
365    }
366
367    builder
368}
369
370/// Transform a Bubba event expression into a Rust EventHandler.
371///
372/// `alert("msg")`          → `EventHandler::new("click", |_| { alert("msg") })`
373/// `log("msg")`            → `EventHandler::new("click", |_| { log_msg("msg") })`
374/// `navigate(ScreenName)`  → `EventHandler::new("click", |_| { navigate_to(stringify!(ScreenName), ScreenName) })`
375/// `my_custom_fn()`        → `EventHandler::new("click", |_| { my_custom_fn() })`
376fn codegen_event_expr(expr: &Expr, event: &str) -> TokenStream2 {
377    // Pattern-match Bubba built-ins, fall back to user expr
378    match expr {
379        Expr::Call(call) => {
380            if let Expr::Path(path) = call.func.as_ref() {
381                let name = path.path.segments.last().map(|s| s.ident.to_string());
382                match name.as_deref() {
383                    Some("alert") => {
384                        let args = &call.args;
385                        return quote! {
386                            ::bubba_core::events::EventHandler::new(#event, move |_| {
387                                ::bubba_core::runtime::alert(#args);
388                            })
389                        };
390                    }
391                    Some("log") => {
392                        let args = &call.args;
393                        return quote! {
394                            ::bubba_core::events::EventHandler::new(#event, move |_| {
395                                ::bubba_core::runtime::log_msg(#args);
396                            })
397                        };
398                    }
399                    Some("navigate") => {
400                        // navigate(Profile) → navigate_to("Profile", Profile)
401                        if let Some(screen_arg) = call.args.first() {
402                            return quote! {
403                                ::bubba_core::events::EventHandler::new(#event, move |_| {
404                                    ::bubba_core::navigation::navigate_to(
405                                        stringify!(#screen_arg),
406                                        #screen_arg,
407                                    );
408                                })
409                            };
410                        }
411                    }
412                    _ => {}
413                }
414            }
415            // Generic call expression
416            quote! {
417                ::bubba_core::events::EventHandler::new(#event, move |_| { #expr; })
418            }
419        }
420        // Closure passed directly: onclick=|_| { ... }
421        Expr::Closure(closure) => {
422            quote! {
423                ::bubba_core::events::EventHandler::new(#event, #closure)
424            }
425        }
426        // Any other expression
427        _ => {
428            quote! {
429                ::bubba_core::events::EventHandler::new(#event, move |_| { #expr; })
430            }
431        }
432    }
433}