cabin_macros/
lib.rs

1mod boundary_attribute;
2mod derive_attribute;
3
4use proc_macro::TokenStream;
5use quote::{quote, ToTokens};
6use syn::parse::{Parse, ParseStream};
7use syn::punctuated::Punctuated;
8use syn::token::{Comma, Dot, Eq, Paren};
9use syn::{parse_macro_input, DeriveInput, Expr, ExprLit, Ident, ItemFn, Path, Type};
10
11#[proc_macro_derive(Attribute, attributes(attribute))]
12pub fn derive_attribute(item: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(item as DeriveInput);
14    match derive_attribute::derive_attribute(input) {
15        Ok(ts) => ts.into(),
16        Err(err) => err.into_compile_error().into(),
17    }
18}
19
20#[proc_macro_attribute]
21pub fn boundary(attr: TokenStream, item: TokenStream) -> TokenStream {
22    let events = parse_macro_input!(attr with Punctuated::<Type, Comma>::parse_terminated);
23    let input = parse_macro_input!(item as ItemFn);
24    match boundary_attribute::boundary_attribute(input, events) {
25        Ok(ts) => ts.into(),
26        Err(err) => err.into_compile_error().into(),
27    }
28}
29
30#[derive(Debug, Hash)]
31struct OptionExpr {
32    key: Ident,
33    value: Option<Expr>,
34}
35
36impl Parse for OptionExpr {
37    fn parse(input: ParseStream) -> syn::Result<Self> {
38        let key = Ident::parse(input)?;
39        let value = if Option::<Eq>::parse(input)?.is_some() {
40            Some(Expr::parse(input)?)
41        } else {
42            None
43        };
44        Ok(OptionExpr { key, value })
45    }
46}
47
48#[derive(Debug, Hash)]
49enum StyleExpr {
50    // e.g.: css::bg::BLACK
51    Path {
52        path: Path,
53        // e.g.:css::bg::BLACK.hover()
54        method_calls: StyleMethodCalls,
55    },
56    // e.g.: css::w::px(46)
57    Call {
58        func: Path,
59        paren_token: Paren,
60        args: Punctuated<ExprLit, Comma>,
61        // e.g.: css::w::px(46).hover()
62        method_calls: StyleMethodCalls,
63    },
64    // e.g.: (css::bg::BLACK, css::w::px(46))
65    Tuple {
66        paren_token: Paren,
67        args: Punctuated<StyleExpr, Comma>,
68        method_calls: StyleMethodCalls,
69    },
70}
71
72#[derive(Debug, Hash, Clone)]
73struct StyleMethodCalls {
74    method_calls: Option<Vec<StyleMethodCall>>,
75}
76
77#[derive(Debug, Hash, Clone)]
78struct StyleMethodCall {
79    dot_token: Dot,
80    method: Option<Ident>,      // optional to allow incomplete inputs
81    paren_token: Option<Paren>, // optional to allow incomplete inputs
82}
83
84impl Parse for StyleExpr {
85    fn parse(input: ParseStream) -> syn::Result<Self> {
86        if input.peek(Paren) {
87            let content;
88            return Ok(StyleExpr::Tuple {
89                paren_token: syn::parenthesized!(content in input),
90                args: content.parse_terminated(StyleExpr::parse, Comma)?,
91                method_calls: input.parse()?,
92            });
93        }
94
95        let path = Path::parse(input)?;
96
97        if input.peek(Paren) {
98            let content;
99            Ok(StyleExpr::Call {
100                func: path,
101                paren_token: syn::parenthesized!(content in input),
102                args: content.parse_terminated(ExprLit::parse, Comma)?,
103                method_calls: input.parse()?,
104            })
105        } else {
106            Ok(StyleExpr::Path {
107                path,
108                method_calls: input.parse()?,
109            })
110        }
111    }
112}
113
114impl Parse for StyleMethodCalls {
115    fn parse(input: ParseStream) -> syn::Result<Self> {
116        let mut method_calls = Vec::with_capacity(1);
117        while input.peek(Dot) {
118            method_calls.push(StyleMethodCall {
119                dot_token: input.parse()?,
120                method: if input.peek(Ident) {
121                    Some(input.parse()?)
122                } else {
123                    None
124                },
125                paren_token: if input.peek(Paren) {
126                    #[allow(unused)]
127                    let content;
128                    Some(syn::parenthesized!(content in input))
129                } else {
130                    None
131                },
132            });
133        }
134
135        Ok(Self {
136            method_calls: if method_calls.is_empty() {
137                None
138            } else {
139                Some(method_calls)
140            },
141        })
142    }
143}
144
145impl ToTokens for StyleExpr {
146    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
147        match self {
148            StyleExpr::Path { path, method_calls } => {
149                path.to_tokens(tokens);
150                method_calls.to_tokens(tokens);
151            }
152            StyleExpr::Call {
153                func,
154                paren_token,
155                args,
156                method_calls,
157            } => {
158                func.to_tokens(tokens);
159                paren_token.surround(tokens, |tokens| {
160                    args.to_tokens(tokens);
161                });
162                method_calls.to_tokens(tokens);
163            }
164            StyleExpr::Tuple {
165                paren_token,
166                args,
167                method_calls,
168            } => {
169                paren_token.surround(tokens, |tokens| {
170                    args.to_tokens(tokens);
171                });
172                method_calls.to_tokens(tokens);
173            }
174        }
175    }
176}
177
178impl ToTokens for StyleMethodCall {
179    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
180        let StyleMethodCall {
181            dot_token,
182            method,
183            paren_token,
184        } = self;
185        dot_token.to_tokens(tokens);
186        method.to_tokens(tokens);
187        if let Some(paren_token) = paren_token {
188            paren_token.surround(tokens, |_| {});
189        }
190    }
191}
192
193impl ToTokens for StyleMethodCalls {
194    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
195        if let Some(method_calls) = &self.method_calls {
196            for method_call in method_calls {
197                method_call.to_tokens(tokens);
198            }
199        }
200    }
201}
202
203#[derive(Debug, Hash)]
204struct Styles {
205    styles: Punctuated<StyleExpr, Comma>,
206}
207
208impl Parse for Styles {
209    fn parse(input: ParseStream) -> syn::Result<Self> {
210        Ok(Styles {
211            styles: Punctuated::<StyleExpr, Comma>::parse_terminated(input)?,
212        })
213    }
214}
215
216fn flatten_recursively(iter: impl Iterator<Item = StyleExpr>) -> impl Iterator<Item = StyleExpr> {
217    iter.flat_map(|style| match style {
218        s @ StyleExpr::Path { .. } => Box::new(std::iter::once(s)),
219        s @ StyleExpr::Call { .. } => Box::new(std::iter::once(s)),
220        StyleExpr::Tuple {
221            args,
222            method_calls: parent_method_calls,
223            ..
224        } => Box::new(flatten_recursively(args.into_iter()).map(move |mut s| {
225            if parent_method_calls.method_calls.is_some() {
226                match &mut s {
227                    StyleExpr::Call { method_calls, .. } | StyleExpr::Path { method_calls, .. } => {
228                        if method_calls.method_calls.is_none() {
229                            method_calls.method_calls = parent_method_calls.method_calls.clone();
230                        } else {
231                            method_calls
232                                .method_calls
233                                .as_mut()
234                                .unwrap()
235                                .extend(parent_method_calls.method_calls.clone().unwrap())
236                        }
237                    }
238                    StyleExpr::Tuple { .. } => unreachable!(),
239                }
240            }
241            s
242        })) as Box<dyn Iterator<Item = StyleExpr>>,
243    })
244}
245
246#[proc_macro]
247pub fn tw(item: TokenStream) -> TokenStream {
248    let input = parse_macro_input!(item as Styles);
249    // dbg!(&input);
250
251    // TODO: Partition here into ones without any modifier, and the ones with modifieres?
252    // Flatten tuples out
253    let styles = flatten_recursively(input.styles.into_iter());
254
255    quote! {
256        {
257            static NAME: ::cabin::private::OnceCell<String> = ::cabin::private::OnceCell::new();
258
259            #[::cabin::private::linkme::distributed_slice(::cabin_tailwind::registry::STYLES)]
260            #[linkme(crate = ::cabin::private::linkme)]
261            fn __register(r: &mut ::cabin_tailwind::registry::StyleRegistry) {
262                let name = r.add(&[#(&#styles,)*]);
263                NAME.set(name).ok();
264            }
265
266            ::cabin_tailwind::Class(::std::borrow::Cow::Borrowed(
267                NAME.get().map(|s| s.as_str()).unwrap_or_default()
268            ))
269        }
270    }
271    .into()
272}