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}