1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::parse::{Parse, ParseStream};
4use syn::{FnArg, ItemFn, Type, parse_macro_input};
5
6#[proc_macro_attribute]
12pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
13 let f = parse_macro_input!(item as ItemFn);
14 if !f.sig.generics.params.is_empty() || f.sig.asyncness.is_some() {
15 return syn::Error::new_spanned(
16 &f.sig,
17 "#[component] functions cannot be generic or async",
18 )
19 .to_compile_error()
20 .into();
21 }
22 let name = &f.sig.ident;
23 let vis = &f.vis;
24 let attrs = &f.attrs;
25 let body = &f.block;
26 let inputs: Vec<&FnArg> = f.sig.inputs.iter().collect();
27 let (props_ty, props_pat, hooks_pat): (Type, Box<syn::Pat>, Box<syn::Pat>) =
28 match inputs.as_slice() {
29 [FnArg::Typed(p), FnArg::Typed(h)] => {
30 let Type::Reference(r) = &*p.ty else {
31 return syn::Error::new_spanned(
32 &p.ty,
33 "props argument must be a reference (&MyProps)",
34 )
35 .to_compile_error()
36 .into();
37 };
38 ((*r.elem).clone(), p.pat.clone(), h.pat.clone())
39 }
40 [FnArg::Typed(h)] => (
41 syn::parse_quote!(()),
42 syn::parse_quote!(_props),
43 h.pat.clone(),
44 ),
45 _ => {
46 return syn::Error::new_spanned(
47 &f.sig,
48 "#[component] fn must take (props: &P, hooks: &mut Hooks) or (hooks: &mut Hooks)",
49 )
50 .to_compile_error()
51 .into();
52 }
53 };
54 quote! {
55 #[allow(non_camel_case_types)]
56 #(#attrs)*
57 #vis struct #name;
58
59 impl ::ntui::Component for #name {
60 type Props = #props_ty;
61 fn render(#props_pat: &#props_ty, #hooks_pat: &mut ::ntui::Hooks) -> ::ntui::Element #body
62 }
63 }
64 .into()
65}
66
67struct ElementNode {
70 name: syn::Ident,
71 props: Vec<(syn::Ident, syn::Expr)>,
72 key: Option<syn::Expr>,
73 children: Vec<Child>,
74}
75
76enum Child {
77 Node(ElementNode),
78 Splice(syn::Expr),
79}
80
81impl Parse for ElementNode {
82 fn parse(input: ParseStream) -> syn::Result<Self> {
83 let name: syn::Ident = input.parse()?;
84 let mut props = Vec::new();
85 let mut key = None;
86 if input.peek(syn::token::Paren) {
87 let content;
88 syn::parenthesized!(content in input);
89 while !content.is_empty() {
90 let pname: syn::Ident = content.parse()?;
91 content.parse::<syn::Token![:]>()?;
92 let expr: syn::Expr = content.parse()?;
93 if pname == "key" {
94 key = Some(expr);
95 } else {
96 props.push((pname, expr));
97 }
98 if content.peek(syn::Token![,]) {
99 content.parse::<syn::Token![,]>()?;
100 }
101 }
102 }
103 let mut children = Vec::new();
104 if input.peek(syn::token::Brace) {
105 let content;
106 syn::braced!(content in input);
107 while !content.is_empty() {
108 if content.peek(syn::Token![#]) {
109 content.parse::<syn::Token![#]>()?;
110 let inner;
111 syn::parenthesized!(inner in content);
112 children.push(Child::Splice(inner.parse()?));
113 } else {
114 children.push(Child::Node(content.parse()?));
115 }
116 }
117 }
118 Ok(ElementNode {
119 name,
120 props,
121 key,
122 children,
123 })
124 }
125}
126
127#[proc_macro]
137pub fn element(input: TokenStream) -> TokenStream {
138 let node = parse_macro_input!(input as ElementNode);
139 gen_node(&node).into()
140}
141
142fn is_unsuffixed_int_lit(expr: &syn::Expr) -> bool {
145 fn lit_is_bare_int(lit: &syn::Lit) -> bool {
146 matches!(lit, syn::Lit::Int(i) if i.suffix().is_empty())
147 }
148 match expr {
149 syn::Expr::Lit(syn::ExprLit { lit, .. }) => lit_is_bare_int(lit),
150 syn::Expr::Unary(syn::ExprUnary {
151 op: syn::UnOp::Neg(_),
152 expr,
153 ..
154 }) => is_unsuffixed_int_lit(expr),
155 syn::Expr::Paren(syn::ExprParen { expr, .. }) => is_unsuffixed_int_lit(expr),
156 _ => false,
157 }
158}
159
160fn gen_node(node: &ElementNode) -> proc_macro2::TokenStream {
161 let name = &node.name;
162 let fields = node.props.iter().map(|(k, v)| {
163 if is_unsuffixed_int_lit(v) {
171 quote! { #k: #v, }
172 } else {
173 quote! { #k: ::core::convert::Into::into(#v), }
174 }
175 });
176 let children = gen_children(&node.children);
177 let name_str = name.to_string();
178 let base = match name_str.as_str() {
179 "View" => quote! {
180 {
181 #[allow(clippy::needless_update)]
185 let __props = ::ntui::ViewProps { #(#fields)* ..::core::default::Default::default() };
186 ::ntui::Element::view(__props, #children)
187 }
188 },
189 "Text" => quote! {
190 {
191 #[allow(clippy::needless_update)]
192 let __props = ::ntui::TextProps { #(#fields)* ..::core::default::Default::default() };
193 ::ntui::Element::text(__props)
194 }
195 },
196 "Fragment" => quote! { ::ntui::Element::fragment(#children) },
197 "ContextProvider" => {
198 let Some((_, v)) = node.props.iter().find(|(k, _)| k == "value") else {
199 return syn::Error::new_spanned(
200 &node.name,
201 "ContextProvider requires a `value:` prop",
202 )
203 .to_compile_error();
204 };
205 quote! { ::ntui::Element::provider(#v, #children) }
206 }
207 _ => {
208 if node.props.is_empty() {
209 quote! { ::ntui::Element::component::<#name>(::core::default::Default::default()) }
210 } else {
211 let props_ty = format_ident!("{}Props", name);
212 quote! {
213 {
214 #[allow(clippy::needless_update)]
218 let __props = #props_ty { #(#fields)* ..::core::default::Default::default() };
219 ::ntui::Element::component::<#name>(__props)
220 }
221 }
222 }
223 }
224 };
225 match &node.key {
226 Some(k) => quote! { #base.with_key(#k) },
227 None => base,
228 }
229}
230
231fn gen_children(children: &[Child]) -> proc_macro2::TokenStream {
232 let stmts = children.iter().map(|c| match c {
233 Child::Node(n) => {
234 let t = gen_node(n);
235 quote! { __children.push(#t); }
236 }
237 Child::Splice(e) => quote! { __children.extend(#e); },
238 });
239 quote! {{
240 let mut __children: ::std::vec::Vec<::ntui::Element> = ::std::vec::Vec::new();
241 #(#stmts)*
242 __children
243 }}
244}