yew_macro/html_tree/
html_component.rs1use 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 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 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 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}