leptos_struct_component_macro/
lib.rs1extern crate proc_macro;
4
5use proc_macro2::TokenStream;
6use quote::quote;
7use syn::{
8 AttrStyle, Attribute, Data, DeriveInput, Expr, ExprArray, GenericArgument, Ident, LitBool,
9 LitStr, Meta, PathArguments, Type, parse_macro_input, spanned::Spanned,
10};
11
12#[derive(Debug, Default)]
13struct StructComponentAttrArgs {
14 tag: Option<String>,
15 dynamic_tag: Option<Vec<(Expr, String)>>,
16 no_children: Option<bool>,
17}
18
19fn parse_struct_component_attr(attr: &Attribute) -> Result<StructComponentAttrArgs, syn::Error> {
20 if !matches!(attr.style, AttrStyle::Outer) {
21 Err(syn::Error::new(attr.span(), "not an inner attribute"))
22 } else if let Meta::List(list) = &attr.meta {
23 let mut args = StructComponentAttrArgs::default();
24
25 list.parse_nested_meta(|meta| {
26 if meta.path.is_ident("tag") {
27 let value = meta.value().and_then(|value| value.parse::<LitStr>())?;
28
29 args.tag = Some(value.value());
30
31 Ok(())
32 } else if meta.path.is_ident("dynamic_tag") {
33 let value = meta.value().and_then(|value| value.parse::<ExprArray>())?;
34
35 args.dynamic_tag = Some(
36 value
37 .elems
38 .into_iter()
39 .filter_map(|elem| match &elem {
40 Expr::Path(path) => path.path.segments.last().map(|segment| {
41 (elem.clone(), segment.ident.to_string().to_lowercase())
42 }),
43 _ => None,
44 })
45 .collect(),
46 );
47
48 Ok(())
49 } else if meta.path.is_ident("no_children") {
50 let value = meta.value().and_then(|value| value.parse::<LitBool>())?;
51
52 args.no_children = Some(value.value());
53
54 Ok(())
55 } else {
56 Err(meta.error("unknown property"))
57 }
58 })?;
59
60 Ok(args)
61 } else {
62 Err(syn::Error::new(attr.span(), "not a list"))
63 }
64}
65
66#[proc_macro_attribute]
67pub fn struct_component(
68 _attr: proc_macro::TokenStream,
69 item: proc_macro::TokenStream,
70) -> proc_macro::TokenStream {
71 item
72}
73
74#[proc_macro_derive(StructComponent, attributes(struct_component))]
75pub fn derive_struct_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
76 let derive_input = parse_macro_input!(input as DeriveInput);
77
78 let mut args = StructComponentAttrArgs::default();
79 for attr in &derive_input.attrs {
80 if attr.path().is_ident("struct_component") {
81 match parse_struct_component_attr(attr) {
82 Ok(result) => {
83 args = result;
84 }
85 Err(error) => {
86 return error.to_compile_error().into();
87 }
88 }
89 }
90 }
91
92 if let Data::Struct(data_struct) = &derive_input.data {
93 let ident = derive_input.ident.clone();
94
95 let mut attributes: Vec<TokenStream> = vec![];
96 let mut listeners: Vec<TokenStream> = vec![];
99 let mut dynamic_tag: Option<(Ident, Vec<(Expr, String)>)> = None;
101 let mut node_ref: Option<TokenStream> = None;
102
103 for field in &data_struct.fields {
104 if let Some(ident) = &field.ident {
105 if let Some(attr) = field
106 .attrs
107 .iter()
108 .find(|attr| attr.path().is_ident("struct_component"))
109 {
110 match parse_struct_component_attr(attr) {
111 Ok(args) => {
112 if let Some(tags) = args.dynamic_tag {
113 dynamic_tag = Some((ident.clone(), tags));
114
115 continue;
116 }
117 }
118 Err(error) => {
119 return error.to_compile_error().into();
120 }
121 }
122 }
123
124 if ident == "attributes" {
125 continue;
141 }
142
143 if ident == "node_ref" {
144 node_ref = Some(quote! {
145 .node_ref(self.node_ref)
146 });
147
148 continue;
149 }
150
151 if ident.to_string().starts_with("on") {
152 if let Type::Path(path) = &field.ty {
153 let event = ident
154 .to_string()
155 .strip_prefix("on")
156 .expect("String should start with `on`.")
157 .parse::<TokenStream>()
158 .expect("String should parse as TokenStream.");
159
160 let first = path.path.segments.first();
161 let first_argument = first.and_then(|segment| match &segment.arguments {
162 PathArguments::None => None,
163 PathArguments::AngleBracketed(arguments) => {
164 arguments.args.first().and_then(|arg| match arg {
165 GenericArgument::Type(Type::Path(path)) => {
166 path.path.segments.first()
167 }
168 _ => None,
169 })
170 }
171 PathArguments::Parenthesized(_) => None,
172 });
173
174 if first.is_some_and(|segment| segment.ident == "Callback") {
175 listeners.push(quote! {
176 .on(::leptos::tachys::html::event::#event, move |event| {
177 self.#ident.run(event);
178 })
179 });
180
181 continue;
182 } else if first.is_some_and(|segment| segment.ident == "Option")
183 && first_argument.is_some_and(|argument| argument.ident == "Callback")
184 {
185 listeners.push(quote! {
186 .on(::leptos::tachys::html::event::#event, move |event| {
187 if let Some(listener) = &self.#ident {
188 listener.run(event);
189 }
190 })
191 });
192
193 continue;
194 }
195 }
196 }
197
198 match &field.ty {
199 Type::Path(path) => {
200 let first = path.path.segments.first();
201
202 attributes.push(
203 if first.is_some_and(|segment| segment.ident == "MaybeProp") {
204 quote! {
205 .#ident(move || self.#ident.get())
206 }
207 } else {
208 quote! {
209 .#ident(self.#ident)
210 }
211 },
212 );
213 }
214 _ => {
215 return syn::Error::new(field.ty.span(), "expected type path")
216 .to_compile_error()
217 .into();
218 }
219 }
220 }
221 }
222
223 let arguments = if args.no_children.unwrap_or(false) {
224 quote! {
225 self
226 }
227 } else {
228 quote! {
229 self, children: Option<::leptos::prelude::Children>
230 }
231 };
232
233 let children = (!args.no_children.unwrap_or(false)).then(|| {
234 quote! {
235 .child(children.map(|children| children()))
236 }
237 });
238
239 let tag_methods = quote! {
240 #node_ref
243 #(#attributes)*
244 #(#listeners)*
245 #children
246 .into_any()
247 };
248
249 if let Some((tag_ident, tags)) = dynamic_tag {
250 let exprs = tags.iter().map(|(expr, _)| expr).collect::<Vec<_>>();
251 let tags = tags
252 .iter()
253 .map(|(_, tag)| {
254 let tag = format!("::leptos::html::{tag}()")
255 .parse::<TokenStream>()
256 .expect("String should parse as TokenStream.");
257
258 quote! {
259 #tag
260 #tag_methods
261 }
262 })
263 .collect::<Vec<_>>();
264
265 quote! {
266 impl #ident {
267 pub fn render(#arguments) -> ::leptos::tachys::view::any_view::AnyView {
268 match self.#tag_ident {
269 #(#exprs => #tags,)*
270 }
271 }
272 }
273 }
274 .into()
275 } else if let Some(tag) = args.tag {
276 let tag = format!("::leptos::html::{tag}()")
277 .parse::<TokenStream>()
278 .expect("String should parse as TokenStream.");
279
280 quote! {
281 impl #ident {
282 pub fn render(#arguments) -> ::leptos::tachys::view::any_view::AnyView {
283 #tag
284 #tag_methods
285 }
286 }
287 }
288 .into()
289 } else {
290 return syn::Error::new(derive_input.span(), "`#[struct_component(tag = \"\")] or #[struct_component(dynamic_tag = true)]` is required")
291 .to_compile_error()
292 .into();
293 }
294 } else {
295 syn::Error::new(derive_input.span(), "expected struct")
296 .to_compile_error()
297 .into()
298 }
299}