This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/html_tree/
html_component.rs

1use proc_macro2::Span;
2use quote::{quote, quote_spanned, ToTokens};
3use syn::parse::discouraged::Speculative;
4use syn::parse::{Parse, ParseStream};
5use syn::spanned::Spanned;
6use syn::{Token, Type};
7
8use super::{HtmlChildrenTree, TagTokens};
9use crate::is_ide_completion;
10use crate::props::ComponentProps;
11
12pub struct HtmlComponent {
13    ty: Type,
14    props: ComponentProps,
15    children: HtmlChildrenTree,
16    close: Option<HtmlComponentClose>,
17}
18
19impl Parse for HtmlComponent {
20    fn parse(input: ParseStream) -> syn::Result<Self> {
21        // check if the next tokens are </
22        let trying_to_close = || {
23            let lt = input.peek(Token![<]);
24            let div = input.peek2(Token![/]);
25            lt && div
26        };
27
28        if trying_to_close() {
29            let close = input.parse::<HtmlComponentClose>();
30            if !is_ide_completion() {
31                return match close {
32                    Ok(close) => Err(syn::Error::new_spanned(
33                        close.to_spanned(),
34                        "this closing tag has no corresponding opening tag",
35                    )),
36                    Err(err) => Err(err),
37                };
38            }
39        }
40
41        let open = input.parse::<HtmlComponentOpen>()?;
42        // Return early if it's a self-closing tag
43        if open.is_self_closing() {
44            return Ok(HtmlComponent {
45                ty: open.ty,
46                props: open.props,
47                children: HtmlChildrenTree::new(),
48                close: None,
49            });
50        }
51
52        let mut children = HtmlChildrenTree::new();
53        let close = loop {
54            if input.is_empty() {
55                if is_ide_completion() {
56                    break None;
57                }
58                return Err(syn::Error::new_spanned(
59                    open.to_spanned(),
60                    "this opening tag has no corresponding closing tag",
61                ));
62            }
63
64            if trying_to_close() {
65                fn format_token_stream(ts: impl ToTokens) -> String {
66                    let string = ts.to_token_stream().to_string();
67                    // remove unnecessary spaces
68                    string.replace(' ', "")
69                }
70
71                let fork = input.fork();
72                let close = TagTokens::parse_end_content(&fork, |i_fork, tag| {
73                    let ty = i_fork.parse().map_err(|e| {
74                        syn::Error::new(
75                            e.span(),
76                            format!(
77                                "expected a valid closing tag for component\nnote: found opening \
78                                 tag `{lt}{0}{gt}`\nhelp: try `{lt}/{0}{gt}`",
79                                format_token_stream(&open.ty),
80                                lt = open.tag.lt.to_token_stream(),
81                                gt = open.tag.gt.to_token_stream(),
82                            ),
83                        )
84                    })?;
85
86                    if ty != open.ty && !is_ide_completion() {
87                        let open_ty = &open.ty;
88                        Err(syn::Error::new_spanned(
89                            quote!(#open_ty #ty),
90                            format!(
91                                "mismatched closing tags: expected `{}`, found `{}`",
92                                format_token_stream(open_ty),
93                                format_token_stream(ty)
94                            ),
95                        ))
96                    } else {
97                        let close = HtmlComponentClose { tag, ty };
98                        input.advance_to(&fork);
99                        Ok(close)
100                    }
101                })?;
102                break Some(close);
103            }
104            children.parse_child(input)?;
105        };
106
107        if !children.is_empty() {
108            if let Some(children_prop) = open.props.children() {
109                return Err(syn::Error::new_spanned(
110                    &children_prop.label,
111                    "cannot specify the `children` prop when the component already has children",
112                ));
113            }
114        }
115
116        Ok(HtmlComponent {
117            ty: open.ty,
118            props: open.props,
119            children,
120            close,
121        })
122    }
123}
124
125impl ToTokens for HtmlComponent {
126    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
127        let Self {
128            ty,
129            props,
130            children,
131            close,
132        } = self;
133
134        let ty_span = ty.span().resolved_at(Span::call_site());
135        let props_ty = quote_spanned!(ty_span=> <#ty as ::yew::html::BaseComponent>::Properties);
136        let children_renderer = children.to_children_renderer_tokens();
137        let build_props = props.build_properties_tokens(&props_ty, children_renderer);
138        let key = props.special().wrap_key_attr();
139        let use_close_tag = close
140            .as_ref()
141            .map(|close| {
142                let close_ty = &close.ty;
143                quote_spanned! {close_ty.span()=>
144                    let _ = |_:#close_ty| {};
145                }
146            })
147            .unwrap_or_default();
148
149        tokens.extend(quote_spanned! {ty_span=>
150            {
151                #use_close_tag
152                #[allow(clippy::let_unit_value)]
153                let __yew_props = #build_props;
154                ::yew::virtual_dom::VChild::<#ty>::new(__yew_props, #key)
155            }
156        });
157    }
158}
159
160struct HtmlComponentOpen {
161    tag: TagTokens,
162    ty: Type,
163    props: ComponentProps,
164}
165impl HtmlComponentOpen {
166    fn is_self_closing(&self) -> bool {
167        self.tag.div.is_some()
168    }
169
170    fn to_spanned(&self) -> impl ToTokens {
171        self.tag.to_spanned()
172    }
173}
174
175impl Parse for HtmlComponentOpen {
176    fn parse(input: ParseStream) -> syn::Result<Self> {
177        TagTokens::parse_start_content(input, |input, tag| {
178            let ty = input.parse()?;
179            let props: ComponentProps = input.parse()?;
180
181            if let Some(ref node_ref) = props.special().node_ref {
182                return Err(syn::Error::new_spanned(
183                    &node_ref.label,
184                    "cannot use `ref` with components. If you want to specify a property, use \
185                     `r#ref` here instead.",
186                ));
187            }
188
189            Ok(Self { tag, ty, props })
190        })
191    }
192}
193
194struct HtmlComponentClose {
195    tag: TagTokens,
196    ty: Type,
197}
198impl HtmlComponentClose {
199    fn to_spanned(&self) -> impl ToTokens {
200        self.tag.to_spanned()
201    }
202}
203
204impl Parse for HtmlComponentClose {
205    fn parse(input: ParseStream) -> syn::Result<Self> {
206        TagTokens::parse_end_content(input, |input, tag| {
207            let ty = input.parse()?;
208            Ok(Self { tag, ty })
209        })
210    }
211}