Skip to main content

builderx_macros/
lib.rs

1//! Procedural macro entry points for the builderx DSL.
2//! The `bx!` macro transforms a nested, JSX-ish syntax into builder calls using
3//! an adapter that knows how to attach children for a concrete toolkit.
4//!
5//! # Examples
6//! Using the facade crate:
7//! ```ignore
8//! use builderx::bx;
9//! let _ = bx! { div[flex]{ "hello" } };
10//! ```
11
12use proc_macro::TokenStream;
13use quote::quote;
14use syn::{
15    Expr, Ident, Path, Result, Token,
16    parse::{Parse, ParseStream},
17    parse_macro_input, parse_quote,
18    punctuated::Punctuated,
19    token,
20};
21
22#[proc_macro]
23pub fn bx(input: TokenStream) -> TokenStream {
24    let input = parse_macro_input!(input as MacroInput);
25    let bx_root: Path = if cfg!(feature = "core-default") {
26        parse_quote!(::builderx_core)
27    } else {
28        parse_quote!(::builderx)
29    };
30    let adapter: Path = input
31        .adapter
32        .unwrap_or_else(|| parse_quote!(#bx_root::DefaultAdapter));
33
34    input.root.to_builder_tokens(&adapter, &bx_root).into()
35}
36
37struct MacroInput {
38    adapter: Option<Path>,
39    root: ViewElement,
40}
41
42impl Parse for MacroInput {
43    fn parse(input: ParseStream) -> Result<Self> {
44        let adapter = if input.peek(Ident) || input.peek(Token![::]) {
45            let fork = input.fork();
46            let _path: Path = fork.parse()?;
47            if fork.peek(Token![=>]) {
48                let path = input.parse()?;
49                input.parse::<Token![=>]>()?;
50                Some(path)
51            } else {
52                None
53            }
54        } else {
55            None
56        };
57
58        let root = input.parse()?;
59        Ok(Self { adapter, root })
60    }
61}
62
63struct ViewElement {
64    tag: Path,
65    args: Option<Punctuated<Expr, Token![,]>>,
66    modifiers: Option<Punctuated<Expr, Token![,]>>,
67    children: Vec<ViewChild>,
68}
69
70enum ViewChild {
71    Element(ViewElement),
72    Expr(Expr),
73    Spread(Expr),
74}
75
76impl Parse for ViewElement {
77    fn parse(input: ParseStream) -> Result<Self> {
78        let tag: Path = input.parse()?;
79
80        let args = if input.peek(token::Paren) {
81            let content;
82            syn::parenthesized!(content in input);
83            Some(Punctuated::parse_terminated(&content)?)
84        } else {
85            None
86        };
87
88        let modifiers = if input.peek(token::Bracket) {
89            let content;
90            syn::bracketed!(content in input);
91            Some(Punctuated::parse_terminated(&content)?)
92        } else {
93            None
94        };
95
96        let mut children = Vec::new();
97        if input.peek(token::Brace) {
98            let content;
99            syn::braced!(content in input);
100            while !content.is_empty() {
101                children.push(content.parse()?);
102                if content.peek(Token![,]) {
103                    content.parse::<Token![,]>()?;
104                }
105            }
106        }
107
108        Ok(ViewElement {
109            tag,
110            args,
111            modifiers,
112            children,
113        })
114    }
115}
116
117impl Parse for ViewChild {
118    fn parse(input: ParseStream) -> Result<Self> {
119        if input.peek(Token![..]) {
120            input.parse::<Token![..]>()?;
121            let expr: Expr = input.parse()?;
122            return Ok(ViewChild::Spread(expr));
123        }
124
125        // Heuristic to detect nested ViewElement vs Expr
126        // If it starts with an Ident or Path, followed by (, [, or {, treat as ViewElement
127        if input.peek(Ident) || input.peek(Token![::]) {
128            let fork = input.fork();
129            if let Ok(_) = fork.parse::<Path>() {
130                if fork.peek(token::Paren) {
131                    let content;
132                    syn::parenthesized!(content in fork);
133                    let _ = Punctuated::<Expr, Token![,]>::parse_terminated(&content);
134                }
135                if fork.peek(token::Bracket) || fork.peek(token::Brace) {
136                    return Ok(ViewChild::Element(input.parse()?));
137                }
138            }
139        }
140
141        // Fallback to Expr
142        Ok(ViewChild::Expr(input.parse()?))
143    }
144}
145
146impl ViewElement {
147    fn to_builder_tokens(&self, adapter: &Path, bx_root: &Path) -> proc_macro2::TokenStream {
148        let tag = &self.tag;
149        let args = &self.args;
150
151        // Start with the tag (constructor)
152        let mut builder = if let Some(args) = args {
153            quote! { #tag(#args) }
154        } else {
155            quote! { #tag() }
156        };
157
158        builder = quote! { <#adapter as #bx_root::BxAdapter>::start(#builder) };
159
160        // Apply modifiers
161        if let Some(mods) = &self.modifiers {
162            for modifier in mods {
163                match modifier {
164                    Expr::Path(_) => {
165                        builder = quote! { #builder.#modifier() };
166                    }
167                    _ => {
168                        builder = quote! { #builder.#modifier };
169                    }
170                }
171            }
172        }
173
174        // Apply children
175        for child in &self.children {
176            builder = match child {
177                ViewChild::Element(el) => {
178                    let child_tokens = el.to_builder_tokens(adapter, bx_root);
179                    quote! { <#adapter as #bx_root::BxAdapter>::attach(#builder, #child_tokens) }
180                }
181                ViewChild::Expr(expr) => {
182                    quote! { <#adapter as #bx_root::BxAdapter>::attach(#builder, #expr) }
183                }
184                ViewChild::Spread(expr) => {
185                    quote! { <#adapter as #bx_root::BxAdapter>::attach_many(#builder, #expr) }
186                }
187            };
188        }
189
190        builder
191    }
192}