yew_struct_component_macro/
lib.rs1extern crate proc_macro;
4
5use proc_macro2::TokenStream;
6use quote::{quote, ToTokens};
7use syn::{
8 parse_macro_input, spanned::Spanned, AttrStyle, Attribute, Data, DeriveInput, Ident, LitBool,
9 LitStr, Meta, Type,
10};
11
12#[derive(Debug, Default)]
13struct StructComponentAttrArgs {
14 tag: Option<String>,
15 dynamic_tag: Option<bool>,
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::<LitBool>())?;
34
35 args.dynamic_tag = Some(value.value());
36
37 Ok(())
38 } else if meta.path.is_ident("no_children") {
39 let value = meta.value().and_then(|value| value.parse::<LitBool>())?;
40
41 args.no_children = Some(value.value());
42
43 Ok(())
44 } else {
45 Err(meta.error("unknown property"))
46 }
47 })?;
48
49 Ok(args)
50 } else {
51 Err(syn::Error::new(attr.span(), "not a list"))
52 }
53}
54
55#[proc_macro_attribute]
56pub fn struct_component(
57 _attr: proc_macro::TokenStream,
58 item: proc_macro::TokenStream,
59) -> proc_macro::TokenStream {
60 item
61}
62
63#[proc_macro_derive(StructComponent, attributes(struct_component))]
64pub fn derive_struct_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
65 let derive_input = parse_macro_input!(input as DeriveInput);
66
67 let mut args = StructComponentAttrArgs::default();
68 for attr in &derive_input.attrs {
69 if attr.path().is_ident("struct_component") {
70 match parse_struct_component_attr(attr) {
71 Ok(result) => {
72 args = result;
73 }
74 Err(error) => {
75 return error.to_compile_error().into();
76 }
77 }
78 }
79 }
80
81 if let Data::Struct(data_struct) = &derive_input.data {
82 let ident = derive_input.ident.clone();
83
84 let mut attributes: Vec<TokenStream> = vec![];
85 let mut attribute_checked: Option<TokenStream> = None;
86 let mut attribute_value: Option<TokenStream> = None;
87 let mut listeners: Vec<Ident> = vec![];
88 let mut attributes_map: Option<TokenStream> = None;
89 let mut tag: Option<TokenStream> = None;
90 let mut node_ref: Option<TokenStream> = None;
91
92 for field in &data_struct.fields {
93 if let Some(ident) = &field.ident {
94 if let Some(attr) = field
95 .attrs
96 .iter()
97 .find(|attr| attr.path().is_ident("struct_component"))
98 {
99 match parse_struct_component_attr(attr) {
100 Ok(args) => {
101 if args.dynamic_tag.is_some_and(|dynamic_tag| dynamic_tag) {
102 tag = Some(quote! {
103 self.#ident.to_string()
104 });
105
106 continue;
107 }
108 }
109 Err(error) => {
110 return error.to_compile_error().into();
111 }
112 }
113 }
114
115 if ident == "attributes" {
116 attributes_map = Some(quote! {
117 .chain(
118 self.attributes
119 .into_iter()
120 .flatten()
121 .flat_map(|(key, value)| value.map(|value| (
122 ::yew::virtual_dom::AttrValue::from(key),
123 ::yew::virtual_dom::AttributeOrProperty::Attribute(AttrValue::from(value)),
124 )),
125 ),
126 )
127 });
128
129 continue;
130 }
131
132 if ident == "node_ref" {
133 node_ref = Some(quote! {
134 tag.node_ref = self.node_ref;
135 });
136
137 continue;
138 }
139
140 if ident.to_string().starts_with("on") {
141 if let Type::Path(path) = &field.ty {
142 let first = path.path.segments.first();
143 if first.is_some_and(|segment| segment.ident == "Callback") {
144 listeners.push(ident.clone());
145
146 continue;
147 }
148 }
149 }
150
151 if ident == "checked" {
152 attribute_checked = Some(quote! {
153 tag.set_checked(self.checked);
154 });
155 }
156
157 if ident == "value" {
158 attribute_value = Some(quote! {
159 tag.set_value(self.value.clone());
160 });
161 }
162
163 match &field.ty {
164 Type::Path(path) => {
165 let name = ident.to_string().replace("_", "-");
166 let name = if name.starts_with("r#") {
167 name.strip_prefix("r#").expect("String should have prefix.")
168 } else {
169 name.as_str()
170 }
171 .to_token_stream();
172
173 let first = path.path.segments.first();
174
175 attributes.push(if first.is_some_and(|segment| segment.ident == "bool") {
176 quote! {
177 self.#ident.then_some((
178 ::yew::virtual_dom::AttrValue::from(#name),
179 ::yew::virtual_dom::AttributeOrProperty::Attribute(
180 ::yew::virtual_dom::AttrValue::from("")
181 ),
182 ))
183 }
184 } else if first.is_some_and(|segment| segment.ident == "AttrValue") {
185 quote! {
186 Some((
187 ::yew::virtual_dom::AttrValue::from(#name),
188 ::yew::virtual_dom::AttributeOrProperty::Attribute(self.#ident),
189 ))
190 }
191 } else if first.is_some_and(|segment| segment.ident == "Option") {
192 quote! {
193 self.#ident.map(|value| (
194 ::yew::virtual_dom::AttrValue::from(#name),
195 ::yew::virtual_dom::AttributeOrProperty::Attribute(
196 ::yew::virtual_dom::AttrValue::from(value)
197 ),
198 ))
199 }
200 } else if first.is_some_and(|segment| segment.ident == "Style") {
201 quote! {
202 self.#ident.as_ref().map(|value| (
203 ::yew::virtual_dom::AttrValue::from(#name),
204 ::yew::virtual_dom::AttributeOrProperty::Attribute(
205 ::yew::virtual_dom::AttrValue::from(value)
206 ),
207 ))
208 }
209 } else {
210 quote! {
211 Some((
212 ::yew::virtual_dom::AttrValue::from(#name),
213 ::yew::virtual_dom::AttributeOrProperty::Attribute(
214 ::yew::virtual_dom::AttrValue::from(self.#ident)
215 ),
216 ))
217 }
218 });
219 }
220 _ => {
221 return syn::Error::new(field.ty.span(), "expected type path")
222 .to_compile_error()
223 .into()
224 }
225 }
226 }
227 }
228
229 let tag = if let Some(tag) =
230 tag.or_else(|| args.tag.map(|tag| tag.as_str().to_token_stream()))
231 {
232 tag
233 } else {
234 return syn::Error::new(derive_input.span(), "`#[struct_component(tag = \"\")] or #[struct_component(dynamic_tag = true)]` is required")
235 .to_compile_error()
236 .into();
237 };
238
239 let arguments = if args.no_children.unwrap_or(false) {
240 quote! {
241 self
242 }
243 } else {
244 quote! {
245 self, children: ::yew::prelude::Html
246 }
247 };
248
249 let children = (!args.no_children.unwrap_or(false)).then(|| {
250 quote! {
251 tag.add_child(children);
252 }
253 });
254
255 quote! {
256 impl #ident {
257 pub fn render(#arguments) -> ::yew::prelude::Html {
258 let mut tag = ::yew::virtual_dom::VTag::new(#tag);
259 #node_ref
260
261 #attribute_checked
262 #attribute_value
263 tag.set_attributes(::yew::virtual_dom::Attributes::IndexMap(
264 ::std::rc::Rc::new(
265 [
266 #(#attributes,)*
267 ]
268 .into_iter()
269 .flatten()
270 #attributes_map
271 .collect(),
272 ),
273 ));
274
275 tag.set_listeners(::std::boxed::Box::new([
276 #(::yew::html::#listeners::Wrapper::__macro_new(
277 self.#listeners,
278 ),)*
279 ]));
280
281 #children
282
283 tag.into()
284 }
285 }
286 }
287 .into()
288 } else {
289 syn::Error::new(derive_input.span(), "expected struct")
290 .to_compile_error()
291 .into()
292 }
293}