yew_html_attributes_macro_derive/
lib.rs

1//! This crate contains the macros for the [yew-html-attributes](https://crates.io/crates/yew-html-attributes) crate.
2
3mod consts;
4mod has_attributes;
5mod use_attributes;
6mod utils;
7
8extern crate proc_macro;
9
10use consts::{ATTRIBUTE_TAG, ELEMENT_ARG, EXCLUDE_ARG, INVISIBLE_ARG, EXCLUDE_ARG_REGEX};
11use has_attributes::transform_struct;
12use quote::{quote};
13use syn::{parse_macro_input, AttributeArgs, DeriveInput, NestedMeta};
14use use_attributes::{generate_set_instructions, generate_unset_instructions};
15
16/// Parse the has_attributes macro arguments
17fn parse_has_attributes_args(args: Vec<NestedMeta>) -> (bool, Option<String>, Vec<String>) {
18  let mut excluded = vec![];
19  let mut visible = true;
20  let mut element = None;
21  for arg in args {
22    if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = arg {
23      if nv.path.is_ident(EXCLUDE_ARG) {
24        if let syn::Lit::Str(lit) = &nv.lit {
25          let ex = lit.value();
26          let re = regex::Regex::new(EXCLUDE_ARG_REGEX).unwrap();
27          excluded = re.find_iter(&ex).map(|m|m.as_str().to_string()).collect();
28
29        } else {
30          panic!("exclude argument expects a string")
31        }
32      }
33      if nv.path.is_ident(INVISIBLE_ARG) {
34        if let syn::Lit::Bool(lit) = &nv.lit {
35          let lit = lit.value();
36          visible = !lit;
37        } else {
38          panic!("invisble argument expects a boolean")
39        }
40      }
41      if nv.path.is_ident(ELEMENT_ARG) {
42        if let syn::Lit::Str(lit) = &nv.lit {
43          let lit = lit.value();
44          element = Some(lit);
45        } else {
46          panic!("element argument expects a string")
47        }
48      }
49    }
50  }
51  (visible, element, excluded)
52}
53
54
55/** 
56 * The attribute macro that add the fields to your props struct. It expect to be used on a struct that derive the `yew::Properties` trait and the `yew_html_attribute::HasHtmlAttributes` trait.
57 * # Arguments
58 * - exclude: A list of attributes to exclude from the struct in a string. [Default: ""]
59 * - invisible: A boolean that mark the target element as not part of the visble html element (Ex : `<script/>`) [Default: false]
60 * - element: A string that specify the html element to use. [Default: "div"]
61 * 
62 * # Example
63 * ```rust
64 * #[has_html_attributes]
65 * #[derive(Debug, Clone, PartialEq, Default, yew::Properties, yew_html_attribute::HasHtmlAttributes)]
66 * pub struct InputProps{}
67 * ```
68 */ 
69#[proc_macro_attribute]
70pub fn has_html_attributes(
71  attr: proc_macro::TokenStream,
72  item: proc_macro::TokenStream,
73) -> proc_macro::TokenStream {
74  // Parse the input tokens into a syntax tree
75  let args = parse_macro_input!(attr as AttributeArgs);
76
77  let (visible, element, excluded) = parse_has_attributes_args(args);
78
79  let input: DeriveInput = syn::parse(item).unwrap();
80  let mut output = input;
81  match &mut output.data {
82    syn::Data::Struct(strct) => {
83      transform_struct(
84        strct,
85        visible,
86        element.as_deref(),
87        &excluded,
88      );
89    }
90    _ => panic!("use_attributes can only be used on structs"),
91  }
92
93  // Check that the struct has a Properties & HasHtmlAttributes derive
94  let mut has_properties = false;
95  let mut has_html_attributes = false;
96  for attr in &output.attrs {
97    if attr.path.is_ident("derive") {
98      if let syn::Meta::List(list) = &attr.parse_meta().unwrap() {
99        for nested in &list.nested {
100          if let syn::NestedMeta::Meta(syn::Meta::Path(path)) = nested {
101            if path.is_ident("Properties") {
102              has_properties = true;
103            }
104            if path.is_ident("HasHtmlAttributes") {
105              has_html_attributes = true;
106            }
107          }
108        }
109      }
110    }
111  }
112
113  if !has_properties {
114    panic!("has_attributes can only be used on structs with a Properties derive");
115  }
116  if !has_html_attributes {
117    panic!("has_attributes can only be used on structs with a HasHtmlAttributes derive");
118  }
119  quote!(
120    #output
121  )
122  .into()
123}
124
125/// Implements the HasHtmlAttributes trait for the given struct
126#[proc_macro_derive(HasHtmlAttributes, attributes(htmlattr, htmlelem))]
127pub fn derive_has_html_attributes(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
128  let input: DeriveInput = syn::parse(item).unwrap();
129  let name = &input.ident;
130
131  let mut attr_fields = vec![];
132
133  match input.data {
134    syn::Data::Struct(data) => {
135      if let syn::Fields::Named(fields) = data.fields {
136        for field in fields.named {
137          if let Some(attr) = field.attrs.first() {
138            if attr.path.is_ident(ATTRIBUTE_TAG) {
139              attr_fields.push(field.ident.unwrap());
140            }
141          }
142        }
143      } else {
144        panic!("HasHtmlAttributes can only be used on structs with named fields");
145      }
146    }
147    _ => panic!("HasHtmlAttributes can only be used on structs"),
148  }
149
150  let set_instructions = generate_set_instructions(&attr_fields);
151  let unset_instructions = generate_unset_instructions(&attr_fields);
152  quote!(
153    impl HasHtmlAttributes for #name {
154      fn set_attributes(&self, node: &web_sys::HtmlElement) -> Vec<wasm_bindgen::closure::Closure<dyn Fn(Event)>> {
155        let mut listeners: Vec<wasm_bindgen::closure::Closure<dyn Fn(Event)>> = Vec::new();
156        #(#set_instructions)*
157        listeners
158      }
159      fn unset_attributes(&self, node: &web_sys::HtmlElement) {
160        #(#unset_instructions)*
161      }
162    }
163  ).into()
164}
165
166
167
168/**
169 * The proc macro that use the `yew_html_attribute::HasHtmlAttributes` trait to set and unset the attributes on the html element. It expect to be used on a struct that derive the `yew::Properties` trait and the `yew_html_attribute::HasHtmlAttributes` trait.
170 * # Arguments
171 * - 1 : The name of the struct that implement the `yew_html_attribute::HasHtmlAttributes` trait
172 * - 2 : The reference to the html element
173 * 
174 * # Example
175 * ```rust, no_run
176 * let node_ref = use_node_ref();
177 * use_attributes!(node_ref, props);
178 * html! {
179 *   <>
180 *     <input ref={node_ref} type="text" />
181 *   </>
182 * }
183 * ```
184*/
185#[proc_macro]
186pub fn use_attributes(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
187  // Parse the input tokens
188  let input = parse_macro_input!(item as AttributeArgs);
189  if input.len() != 2 {
190    panic!("use_attributes takes 2 arguments");
191  }
192  let node_ref = match &input[0] {
193    syn::NestedMeta::Meta(syn::Meta::Path(path)) => path.get_ident().unwrap(),
194    _ => panic!("use_attributes first argument must be a path"),
195  };
196  let props = match &input[1] {
197    syn::NestedMeta::Meta(syn::Meta::Path(path)) => path.get_ident().unwrap(),
198    _ => panic!("use_attributes second argument must be a path"),
199  };
200
201  quote!(
202    use_effect_with_deps(|(node_ref, props)|{
203      let node = node_ref.cast::<web_sys::HtmlElement>().unwrap();
204      let props = props.clone();
205      let mut listeners: Vec<wasm_bindgen::closure::Closure::<dyn Fn(Event)>> = props.set_attributes(&node);
206      move || {
207        let node = node;
208        props.unset_attributes(&node);
209        drop(listeners);
210      }
211    }
212    , (#node_ref.clone(), #props.clone()))
213  )
214  .into()
215}