bloom_rsx/
lib.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Expr, ExprPath, Fields};
4use syn_rsx::{parse2, Node, NodeName};
5
6/// The core rsx macro.
7/// Transforms
8/// * `<Component prop="value" />` into `Component::new().prop("value").build().into()`
9/// * `<tag attribute="value" on_event={handler} />` into `tag("tag").attr("attribute", "value").on("event", handler).build().into()`
10/// * `"text"` into `"text".to_string().into()`
11#[proc_macro]
12pub fn rsx(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
13    let tree = parse2(tokens.into()).expect("Failed to parse RSX");
14
15    transform_children(tree).into()
16}
17
18fn transform_node(node: Node) -> TokenStream {
19    match node {
20        Node::Element(element) => match &element.name {
21            NodeName::Block(_) => transform_tag(element.name, element.attributes, element.children),
22            NodeName::Path(path) => {
23                if let Some(ident) = path.path.get_ident() {
24                    if ident
25                        .to_string()
26                        .chars()
27                        .nth(0)
28                        .expect("Cannot render empty identifier")
29                        .is_lowercase()
30                    {
31                        transform_tag(element.name, element.attributes, element.children)
32                    } else {
33                        transform_component(path, element.attributes, element.children)
34                    }
35                } else {
36                    transform_component(path, element.attributes, element.children)
37                }
38            }
39            NodeName::Punctuated(_) => {
40                transform_tag(element.name, element.attributes, element.children)
41            }
42        },
43        Node::Attribute(_) => {
44            panic!("Invalid attribute")
45        }
46        Node::Block(block) => {
47            let value: &Expr = block.value.as_ref();
48            quote! {
49                #value.into()
50            }
51        }
52        Node::Comment(_) => TokenStream::new(),
53        Node::Doctype(_) => TokenStream::new(),
54        Node::Fragment(fragment) => transform_children(fragment.children),
55        Node::Text(text) => {
56            let _text: &Expr = text.value.as_ref();
57            quote! { #_text.to_string().into() }
58        }
59    }
60}
61
62fn transform_attributes(attributes: Vec<Node>) -> TokenStream {
63    let mut attrs = TokenStream::new();
64    attributes
65        .into_iter()
66        .map(|attribute| match attribute {
67            Node::Attribute(attribute) => {
68                let name = attribute.key.to_string();
69
70                if name == "ref" {
71                    let _value: Expr = attribute.value.expect("Refs must be Arc<DomRef>").into();
72                    quote! {
73                        .dom_ref(#_value)
74                    }
75                } else if name.starts_with("on_") {
76                    let _value: Expr = attribute.value.expect("Callbacks must be functions").into();
77                    let name = name[3..].to_string();
78                    quote! {
79                        .on(#name, #_value)
80                    }
81                } else {
82                    if let Some(value) = attribute.value {
83                        let _value: Expr = value.into();
84                        quote! {
85                            .attr(#name, #_value)
86                        }
87                    } else {
88                        quote! {
89                            .attr(#name, true)
90                        }
91                    }
92                }
93            }
94            _ => panic!("not an attribute"),
95        })
96        .for_each(|attr| attrs.extend(attr));
97    attrs
98}
99
100fn transform_props(attributes: Vec<Node>) -> TokenStream {
101    let mut props = TokenStream::new();
102    attributes
103        .into_iter()
104        .map(|attribute| match attribute {
105            Node::Attribute(attribute) => {
106                let name = attribute.key.to_string();
107
108                if let Some(value) = attribute.value {
109                    let value: Expr = value.into();
110                    quote! {
111                        .#name(#value)
112                    }
113                } else {
114                    quote! {
115                        .#name(true)
116                    }
117                }
118            }
119            _ => panic!("not an attribute"),
120        })
121        .for_each(|attr| props.extend(attr));
122    props
123}
124
125fn transform_children(nodes: Vec<Node>) -> proc_macro2::TokenStream {
126    let nodes = nodes.into_iter().map(transform_node);
127    let len = nodes.len();
128
129    quote! {
130        {
131            let mut children = Vec::with_capacity(#len);
132            #(children.push(#nodes);)*
133            children.into()
134        }
135    }
136}
137
138fn transform_component(tag: &ExprPath, attributes: Vec<Node>, children: Vec<Node>) -> TokenStream {
139    let attributes = transform_props(attributes);
140    let children = if children.is_empty() {
141        TokenStream::new()
142    } else {
143        let children = transform_children(children);
144        quote! {
145            .children(#children)
146        }
147    };
148    quote! {
149        <#tag>::new()#children #attributes.build().into()
150    }
151}
152
153fn transform_tag(tag: NodeName, attributes: Vec<Node>, children: Vec<Node>) -> TokenStream {
154    let attributes = transform_attributes(attributes);
155    let children = if children.is_empty() {
156        quote! {
157            .into()
158        }
159    } else {
160        let children = transform_children(children);
161        quote! {
162            .children(#children)
163        }
164    };
165    let tag = tag.to_string();
166    quote! {
167        tag(#tag)#attributes.build()#children
168    }
169}
170
171#[proc_macro_derive(NoopBuilder)]
172pub fn derive_noop_builder(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
173    let DeriveInput { ident, data, .. } = parse_macro_input!(item);
174
175    if let Data::Struct(DataStruct { fields, .. }) = data {
176        assert_eq!(
177            fields,
178            Fields::Unit,
179            "NoopBuilder can only be derived for unit structs"
180        );
181    } else {
182        panic!("NoopBuilder can only be derived for unit structs")
183    }
184
185    quote! {
186        impl #ident {
187            fn new() -> Self {
188                Self
189            }
190
191            fn build(self) -> Self {
192                self
193            }
194        }
195    }
196    .into()
197}
198
199#[cfg(test)]
200mod tests {
201    use quote::quote;
202
203    #[test]
204    fn transform_text() {
205        let actual = super::transform_node(
206            syn_rsx::parse2(quote! { "hello world" })
207                .unwrap()
208                .into_iter()
209                .nth(0)
210                .unwrap(),
211        );
212        assert_eq!(
213            actual.to_string(),
214            "\"hello world\" . to_string () . into ()"
215        );
216    }
217
218    #[test]
219    fn pass_ref() {
220        let actual = super::transform_node(
221            syn_rsx::parse2(quote! { <div ref={my_ref}></div> })
222                .unwrap()
223                .into_iter()
224                .nth(0)
225                .unwrap(),
226        );
227        assert_eq!(
228            actual.to_string(),
229            "tag (\"div\") . dom_ref ({ my_ref }) . build () . into ()"
230        );
231    }
232
233    #[test]
234    fn render_component() {
235        let actual = super::transform_node(
236            syn_rsx::parse2(quote! {
237                <MyComponent number_prop=123 boolean_prop>
238                  <div id="child" />
239                </MyComponent>
240            })
241            .unwrap()
242            .into_iter()
243            .nth(0)
244            .unwrap(),
245        );
246        assert_eq!(actual.to_string(), "< MyComponent > :: new () . children ({ let mut children = Vec :: with_capacity (1usize) ; children . push (tag (\"div\") . attr (\"id\" , \"child\") . build () . into ()) ; children . into () }) . \"number_prop\" (123) . \"boolean_prop\" (true) . build () . into ()")
247    }
248}