Skip to main content

repose_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Expr, Ident, Token, braced,
5    parse::{Parse, ParseStream},
6};
7
8struct ViewMacro {
9    layout: Option<Ident>,
10    modifiers: Vec<(Ident, Option<Expr>)>,
11    children: Vec<Expr>,
12}
13
14impl Parse for ViewMacro {
15    fn parse(input: ParseStream) -> syn::Result<Self> {
16        // If it's a single expression, treat as pass-through
17        if input.peek(syn::token::Paren) || input.peek(syn::token::Bracket) {
18            return Err(syn::Error::new(input.span(), "unexpected delimiters"));
19        }
20
21        // Parse optional layout identifier (followed by either { or ( )
22        let layout = if input.peek(Ident)
23            && (input.peek2(syn::token::Brace) || input.peek2(syn::token::Paren))
24        {
25            let ident: Ident = input.parse()?;
26            Some(ident)
27        } else {
28            None
29        };
30
31        // Parse optional modifier args: (key: val, ...)
32        let modifiers = if input.peek(syn::token::Paren) {
33            let content;
34            syn::parenthesized!(content in input);
35            let mut mods = Vec::new();
36            while !content.is_empty() {
37                let name: Ident = content.parse()?;
38                let value = if content.peek(Token![:]) {
39                    content.parse::<Token![:]>()?;
40                    Some(content.parse::<Expr>()?)
41                } else {
42                    None
43                };
44                mods.push((name, value));
45                if content.peek(Token![,]) {
46                    content.parse::<Token![,]>()?;
47                } else if content.is_empty() {
48                    break;
49                } else {
50                    return Err(syn::Error::new(
51                        content.span(),
52                        "expected `,` between modifier args",
53                    ));
54                }
55            }
56            mods
57        } else {
58            Vec::new()
59        };
60
61        // Parse children block: { expr, expr, ... }
62        let children = if input.peek(syn::token::Brace) {
63            let content;
64            braced!(content in input);
65            let mut kids = Vec::new();
66            while !content.is_empty() {
67                let expr: Expr = content.parse()?;
68                kids.push(expr);
69                if content.peek(Token![,]) {
70                    content.parse::<Token![,]>()?;
71                } else if content.is_empty() {
72                    break;
73                } else {
74                    return Err(syn::Error::new(
75                        content.span(),
76                        "expected `,` between children",
77                    ));
78                }
79            }
80            kids
81        } else {
82            Vec::new()
83        };
84
85        Ok(Self {
86            layout,
87            modifiers,
88            children,
89        })
90    }
91}
92
93/// A view tree builder macro.
94///
95/// # Example
96///
97/// ```ignore
98/// // Pass-through single expression:
99/// View!(Text("hello"))
100///
101/// // Layout with children:
102/// View! {
103///     Column {
104///         Text("Hello"),
105///         Text("World"),
106///     }
107/// }
108///
109/// // With modifier args:
110/// View! {
111///     Column(padding: 16.0, gap: 8.0) {
112///         Text("Hello"),
113///         Text("World"),
114///     }
115/// }
116/// ```
117#[proc_macro]
118#[allow(non_snake_case)]
119pub fn View(input: TokenStream) -> TokenStream {
120    // Try ViewMacro parser first (handles `Ident { ... }` and `Ident(m: v) { ... }`)
121    let cloned = input.clone();
122    match syn::parse::<ViewMacro>(cloned) {
123        Ok(m) => expand_view(m).into(),
124        Err(macro_err) => {
125            if let Ok(expr) = syn::parse::<Expr>(input) {
126                return quote!(#expr).into();
127            }
128            macro_err.to_compile_error().into()
129        }
130    }
131}
132
133fn expand_view(m: ViewMacro) -> proc_macro2::TokenStream {
134    let ViewMacro {
135        layout,
136        modifiers,
137        children,
138    } = m;
139
140    let compile_err = || {
141        syn::Error::new(
142            proc_macro2::Span::call_site(),
143            "View!: expected a single expression or `Layout(modifiers) { children }`",
144        )
145        .to_compile_error()
146    };
147
148    if children.is_empty() && modifiers.is_empty() {
149        return compile_err();
150    }
151
152    let mod_calls = modifiers.iter().map(|(name, value)| {
153        if let Some(val) = value {
154            quote!(.#name(#val))
155        } else {
156            quote!(.#name())
157        }
158    });
159
160    if children.is_empty() {
161        // Layout with modifiers but no children
162        if modifiers.is_empty() {
163            compile_err()
164        } else if let Some(layout) = layout {
165            quote! {
166                ::repose_ui::#layout(::repose_core::Modifier::new() #(#mod_calls)*)
167            }
168        } else {
169            quote! {
170                ::repose_ui::Column(::repose_core::Modifier::new() #(#mod_calls)*)
171            }
172        }
173    } else if let Some(layout) = layout {
174        // Layout with children
175        let child_exprs = &children;
176        quote! {
177            ::repose_ui::#layout(::repose_core::Modifier::new() #(#mod_calls)*)
178                .child((#(#child_exprs,)*))
179        }
180    } else {
181        // Bare children without layout: wrap in Column
182        let child_exprs = &children;
183        let mod_tokens = if modifiers.is_empty() {
184            quote!(::repose_core::Modifier::new())
185        } else {
186            quote!(::repose_core::Modifier::new() #(#mod_calls)*)
187        };
188        quote! {
189            ::repose_ui::Column(#mod_tokens).child((#(#child_exprs,)*))
190        }
191    }
192}