sycamore_macro/
view.rs

1//! Codegen for `view!` macro.
2//!
3//! Implementation note: We are not using the `quote::ToTokens` trait because we need to pass
4//! additional information to the codegen such as which mode (Client, Hydrate, SSR), etc...
5
6use proc_macro2::TokenStream;
7use quote::quote;
8use sycamore_view_parser::ir::{DynNode, Node, Prop, PropType, Root, TagIdent, TagNode, TextNode};
9use syn::{Expr, Pat};
10
11pub struct Codegen {
12    // TODO: configure mode: Client, Hydrate, SSR
13}
14
15impl Codegen {
16    pub fn root(&self, root: &Root) -> TokenStream {
17        match &root.0[..] {
18            [] => quote! {
19                ::sycamore::rt::View::new()
20            },
21            [node] => self.node(node),
22            nodes => {
23                let nodes = nodes.iter().map(|node| self.node(node));
24                quote! {
25                    ::std::convert::Into::<::sycamore::rt::View>::into(::std::vec![#(#nodes),*])
26                }
27            }
28        }
29    }
30
31    /// Generate a `View` from a `Node`.
32    pub fn node(&self, node: &Node) -> TokenStream {
33        match node {
34            Node::Tag(tag) => {
35                if is_component(&tag.ident) {
36                    self.component(tag)
37                } else {
38                    self.element(tag)
39                }
40            }
41            Node::Text(TextNode { value }) => quote! {
42                ::std::convert::Into::<::sycamore::rt::View>::into(#value)
43            },
44            Node::Dyn(DynNode { value }) => {
45                let is_dynamic = is_dyn(value);
46                if is_dynamic {
47                    quote! {
48                        ::sycamore::rt::View::from_dynamic(
49                            move || ::std::convert::Into::<::sycamore::rt::View>::into(#value)
50                        )
51                    }
52                } else {
53                    quote! {
54                        ::std::convert::Into::<::sycamore::rt::View>::into(#value)
55                    }
56                }
57            }
58        }
59    }
60
61    pub fn element(&self, element: &TagNode) -> TokenStream {
62        let TagNode {
63            ident,
64            props,
65            children,
66        } = element;
67
68        let attributes = props.iter().map(|attr| self.attribute(attr));
69
70        let children = children
71            .0
72            .iter()
73            .map(|child| self.node(child))
74            .collect::<Vec<_>>();
75
76        match ident {
77            TagIdent::Path(tag) => {
78                assert!(tag.get_ident().is_some(), "elements must be an ident");
79                quote! {
80                    ::sycamore::rt::View::from(
81                        ::sycamore::rt::tags::#tag().children(::std::vec![#(#children),*])#(#attributes)*
82                    )
83                }
84            }
85            TagIdent::Hyphenated(tag) => quote! {
86                ::sycamore::rt::View::from(
87                    ::sycamore::rt::custom_element(#tag).children(::std::vec![#(#children),*])#(#attributes)*
88                )
89            },
90        }
91    }
92
93    pub fn attribute(&self, attr: &Prop) -> TokenStream {
94        let value = &attr.value;
95        let is_dynamic = is_dyn(value);
96        let dyn_value = if is_dynamic {
97            quote! { move || #value }
98        } else {
99            quote! { #value }
100        };
101        match &attr.ty {
102            PropType::Plain { ident } => {
103                quote! { .#ident(#dyn_value) }
104            }
105            PropType::PlainHyphenated { ident } => {
106                quote! { .attr(#ident, #dyn_value) }
107            }
108            PropType::PlainQuoted { ident } => {
109                quote! { .attr(#ident, #dyn_value) }
110            }
111            PropType::Directive { dir, ident } => match dir.to_string().as_str() {
112                "on" => quote! { .on(::sycamore::rt::events::#ident, #value) },
113                "prop" => {
114                    let ident = ident.to_string();
115                    quote! { .prop(#ident, #dyn_value) }
116                }
117                "bind" => quote! { .bind(::sycamore::rt::bind::#ident, #value) },
118                _ => syn::Error::new(dir.span(), format!("unknown directive `{dir}`"))
119                    .to_compile_error(),
120            },
121            PropType::Ref => quote! { .r#ref(#value) },
122            PropType::Spread => quote! { .spread(#value) },
123        }
124    }
125
126    pub fn component(
127        &self,
128        TagNode {
129            ident,
130            props,
131            children,
132        }: &TagNode,
133    ) -> TokenStream {
134        let ident = match ident {
135            TagIdent::Path(path) => path,
136            TagIdent::Hyphenated(_) => unreachable!("hyphenated tags are not components"),
137        };
138
139        let plain = props
140            .iter()
141            .filter_map(|prop| match &prop.ty {
142                PropType::Plain { ident } => Some((ident, prop.value.clone())),
143                _ => None,
144            })
145            .collect::<Vec<_>>();
146        let plain_names = plain.iter().map(|(ident, _)| ident);
147        let plain_values = plain.iter().map(|(_, value)| value);
148
149        let other_props = props
150            .iter()
151            .filter(|prop| !matches!(&prop.ty, PropType::Plain { .. }))
152            .collect::<Vec<_>>();
153        let other_attributes = other_props.iter().map(|prop| self.attribute(prop));
154
155        let children_quoted = if children.0.is_empty() {
156            quote! {}
157        } else {
158            let codegen = Codegen {};
159            let children = codegen.root(children);
160            quote! {
161                .children(
162                    ::sycamore::rt::Children::new(move || {
163                        #children
164                    })
165                )
166            }
167        };
168        quote! {{
169            let __component = &#ident; // We do this to make sure the compiler can infer the value for `<G>`.
170            ::sycamore::rt::component_scope(move || ::sycamore::rt::Component::create(
171                __component,
172                ::sycamore::rt::element_like_component_builder(__component)
173                    #(.#plain_names(#plain_values))*
174                    #(#other_attributes)*
175                    #children_quoted
176                    .build()
177            ))
178        }}
179    }
180}
181
182fn is_component(ident: &TagIdent) -> bool {
183    match ident {
184        TagIdent::Path(path) => {
185            path.get_ident().is_none()
186                || path
187                    .get_ident()
188                    .unwrap()
189                    .to_string()
190                    .chars()
191                    .next()
192                    .unwrap()
193                    .is_ascii_uppercase()
194        }
195        // A hyphenated tag is always a custom-element and therefore never a component.
196        TagIdent::Hyphenated(_) => false,
197    }
198}
199
200fn is_dyn(ex: &Expr) -> bool {
201    match ex {
202        Expr::Lit(_) | Expr::Closure(_) | Expr::Path(_) | Expr::Field(_) => false,
203
204        Expr::Paren(p) => is_dyn(&p.expr),
205        Expr::Group(g) => is_dyn(&g.expr),
206        Expr::Tuple(t) => t.elems.iter().any(is_dyn),
207        Expr::Array(a) => a.elems.iter().any(is_dyn),
208        Expr::Repeat(r) => is_dyn(&r.expr) || is_dyn(&r.len),
209        Expr::Struct(s) => s.fields.iter().any(|fv: &syn::FieldValue| is_dyn(&fv.expr)),
210
211        Expr::Cast(c) => is_dyn(&c.expr),
212        Expr::Macro(m) => is_dyn_macro(&m.mac),
213        Expr::Block(b) => is_dyn_block(&b.block),
214        Expr::Const(_const_block) => false,
215
216        Expr::Loop(l) => is_dyn_block(&l.body),
217        Expr::While(w) => is_dyn(&w.cond) || is_dyn_block(&w.body),
218        Expr::ForLoop(f) => is_dyn_pattern(&f.pat) || is_dyn(&f.expr) || is_dyn_block(&f.body),
219        Expr::Break(_) | Expr::Continue(_) => false,
220
221        Expr::Let(e) => is_dyn_pattern(&e.pat) || is_dyn(&e.expr),
222
223        Expr::Match(m) => {
224            is_dyn(&m.expr)
225                || m.arms.iter().any(|a: &syn::Arm| {
226                    is_dyn_pattern(&a.pat)
227                        || a.guard.as_ref().is_some_and(|(_, g_expr)| is_dyn(g_expr))
228                        || is_dyn(&a.body)
229                })
230        }
231
232        Expr::If(i) => {
233            is_dyn(&i.cond)
234                || is_dyn_block(&i.then_branch)
235                || i.else_branch.as_ref().is_some_and(|(_, e)| is_dyn(e))
236        }
237
238        Expr::Unary(u) => is_dyn(&u.expr),
239        Expr::Binary(b) => is_dyn(&b.left) || is_dyn(&b.right),
240        Expr::Index(i) => is_dyn(&i.expr) || is_dyn(&i.index),
241        Expr::Range(r) => {
242            r.start.as_deref().is_some_and(is_dyn) || r.end.as_deref().is_some_and(is_dyn)
243        }
244
245        _ => true,
246    }
247}
248
249fn is_dyn_pattern(pat: &Pat) -> bool {
250    match pat {
251        Pat::Wild(_) | Pat::Lit(_) | Pat::Path(_) | Pat::Rest(_) | Pat::Type(_) | Pat::Const(_) => {
252            false
253        }
254
255        Pat::Paren(p) => is_dyn_pattern(&p.pat),
256        Pat::Or(o) => o.cases.iter().any(is_dyn_pattern),
257        Pat::Tuple(t) => t.elems.iter().any(is_dyn_pattern),
258        Pat::TupleStruct(s) => s.elems.iter().any(is_dyn_pattern),
259        Pat::Slice(s) => s.elems.iter().any(is_dyn_pattern),
260        Pat::Range(r) => {
261            r.start.as_deref().is_some_and(is_dyn) || r.end.as_deref().is_some_and(is_dyn)
262        }
263
264        Pat::Reference(r) => r.mutability.is_some(),
265        Pat::Ident(id) => {
266            (id.by_ref.is_some() && id.mutability.is_some())
267                || id
268                    .subpat
269                    .as_ref()
270                    .is_some_and(|(_, pat)| is_dyn_pattern(pat))
271        }
272
273        Pat::Struct(s) => s
274            .fields
275            .iter()
276            .any(|fp: &syn::FieldPat| is_dyn_pattern(&fp.pat)),
277
278        // syn::Pat is non-exhaustive
279        _ => true,
280    }
281}
282
283fn is_dyn_macro(m: &syn::Macro) -> bool {
284    // Bodies of nested inner view! macros will be checked for dynamic
285    // parts when their own codegen is run.
286    !m.path
287        .get_ident()
288        .is_some_and(|ident| "view" == &ident.to_string())
289}
290
291fn is_dyn_block(block: &syn::Block) -> bool {
292    block.stmts.iter().any(|s: &syn::Stmt| match s {
293        syn::Stmt::Expr(ex, _) => is_dyn(ex),
294        syn::Stmt::Macro(m) => is_dyn_macro(&m.mac),
295        syn::Stmt::Local(loc) => {
296            is_dyn_pattern(&loc.pat)
297                || loc.init.as_ref().is_some_and(|i| {
298                    is_dyn(&i.expr) || i.diverge.as_ref().is_some_and(|(_, ex)| is_dyn(ex))
299                })
300        }
301        syn::Stmt::Item(_) => false,
302    })
303}