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

yew_macro/html_tree/
mod.rs

1use proc_macro2::{Delimiter, Ident, Span, TokenStream};
2use quote::{quote, quote_spanned, ToTokens};
3use syn::buffer::Cursor;
4use syn::ext::IdentExt;
5use syn::parse::{Parse, ParseStream};
6use syn::spanned::Spanned;
7use syn::{braced, token, Token};
8
9use crate::{is_ide_completion, PeekValue};
10
11mod html_block;
12mod html_component;
13mod html_dashed_name;
14mod html_element;
15mod html_for;
16mod html_if;
17mod html_iterable;
18mod html_list;
19mod html_node;
20mod lint;
21mod tag;
22
23use html_block::HtmlBlock;
24use html_component::HtmlComponent;
25pub use html_dashed_name::HtmlDashedName;
26use html_element::HtmlElement;
27use html_if::HtmlIf;
28use html_iterable::HtmlIterable;
29use html_list::HtmlList;
30use html_node::HtmlNode;
31use tag::TagTokens;
32
33use self::html_block::BlockContent;
34use self::html_for::HtmlFor;
35
36pub enum HtmlType {
37    Block,
38    Component,
39    List,
40    Element,
41    If,
42    For,
43    Empty,
44}
45
46pub enum HtmlTree {
47    Block(Box<HtmlBlock>),
48    Component(Box<HtmlComponent>),
49    List(Box<HtmlList>),
50    Element(Box<HtmlElement>),
51    If(Box<HtmlIf>),
52    For(Box<HtmlFor>),
53    Empty,
54}
55
56impl Parse for HtmlTree {
57    fn parse(input: ParseStream) -> syn::Result<Self> {
58        let html_type = Self::peek_html_type(input)
59            .ok_or_else(|| input.error("expected a valid html element"))?;
60        Ok(match html_type {
61            HtmlType::Empty => Self::Empty,
62            HtmlType::Component => Self::Component(Box::new(input.parse()?)),
63            HtmlType::Element => Self::Element(Box::new(input.parse()?)),
64            HtmlType::Block => Self::Block(Box::new(input.parse()?)),
65            HtmlType::List => Self::List(Box::new(input.parse()?)),
66            HtmlType::If => Self::If(Box::new(input.parse()?)),
67            HtmlType::For => Self::For(Box::new(input.parse()?)),
68        })
69    }
70}
71
72impl HtmlTree {
73    /// Determine the [`HtmlType`] before actually parsing it.
74    /// Even though this method accepts a [`ParseStream`], it is forked and the original stream is
75    /// not modified. Once a certain `HtmlType` can be deduced for certain, the function eagerly
76    /// returns with the appropriate type. If invalid html tag, returns `None`.
77    fn peek_html_type(input: ParseStream) -> Option<HtmlType> {
78        let input = input.fork(); // do not modify original ParseStream
79        let cursor = input.cursor();
80
81        if input.is_empty() {
82            Some(HtmlType::Empty)
83        } else if HtmlBlock::peek(cursor).is_some() {
84            Some(HtmlType::Block)
85        } else if HtmlIf::peek(cursor).is_some() {
86            Some(HtmlType::If)
87        } else if HtmlFor::peek(cursor).is_some() {
88            Some(HtmlType::For)
89        } else if input.peek(Token![<]) {
90            let _lt: Token![<] = input.parse().ok()?;
91
92            // eat '/' character for unmatched closing tag
93            let _slash: Option<Token![/]> = input.parse().ok();
94
95            if input.peek(Token![>]) {
96                Some(HtmlType::List)
97            } else if input.peek(Token![@]) {
98                Some(HtmlType::Element) // dynamic element
99            } else if input.peek(Token![::]) {
100                Some(HtmlType::Component)
101            } else if input.peek(Ident::peek_any) {
102                let ident = Ident::parse_any(&input).ok()?;
103                let ident_str = ident.to_string();
104
105                if input.peek(Token![=]) || (input.peek(Token![?]) && input.peek2(Token![=])) {
106                    Some(HtmlType::List)
107                } else if ident_str.chars().next().unwrap().is_ascii_uppercase()
108                    || input.peek(Token![::])
109                    || is_ide_completion() && ident_str.chars().any(|c| c.is_ascii_uppercase())
110                {
111                    Some(HtmlType::Component)
112                } else {
113                    Some(HtmlType::Element)
114                }
115            } else {
116                None
117            }
118        } else {
119            None
120        }
121    }
122}
123
124impl ToTokens for HtmlTree {
125    fn to_tokens(&self, tokens: &mut TokenStream) {
126        lint::lint_all(self);
127        match self {
128            Self::Empty => tokens.extend(quote! {
129                <::yew::virtual_dom::VNode as ::std::default::Default>::default()
130            }),
131            Self::Component(comp) => comp.to_tokens(tokens),
132            Self::Element(tag) => tag.to_tokens(tokens),
133            Self::List(list) => list.to_tokens(tokens),
134            Self::Block(block) => block.to_tokens(tokens),
135            Self::If(block) => block.to_tokens(tokens),
136            Self::For(block) => block.to_tokens(tokens),
137        }
138    }
139}
140
141pub enum HtmlRoot {
142    Tree(HtmlTree),
143    Node(Box<HtmlNode>),
144}
145
146impl Parse for HtmlRoot {
147    fn parse(input: ParseStream) -> syn::Result<Self> {
148        let html_root = if HtmlTree::peek_html_type(input).is_some() {
149            Self::Tree(input.parse()?)
150        } else {
151            Self::Node(Box::new(input.parse()?))
152        };
153
154        if !input.is_empty() {
155            let stream: TokenStream = input.parse()?;
156            Err(syn::Error::new_spanned(
157                stream,
158                "only one root html element is allowed (hint: you can wrap multiple html elements \
159                 in a fragment `<></>`)",
160            ))
161        } else {
162            Ok(html_root)
163        }
164    }
165}
166
167impl ToTokens for HtmlRoot {
168    fn to_tokens(&self, tokens: &mut TokenStream) {
169        match self {
170            Self::Tree(tree) => tree.to_tokens(tokens),
171            Self::Node(node) => node.to_tokens(tokens),
172        }
173    }
174}
175
176/// Same as HtmlRoot but always returns a VNode.
177pub struct HtmlRootVNode(HtmlRoot);
178impl Parse for HtmlRootVNode {
179    fn parse(input: ParseStream) -> syn::Result<Self> {
180        input.parse().map(Self)
181    }
182}
183
184impl ToTokens for HtmlRootVNode {
185    fn to_tokens(&self, tokens: &mut TokenStream) {
186        let new_tokens = self.0.to_token_stream();
187        tokens.extend(
188            quote_spanned! {self.0.span().resolved_at(Span::mixed_site())=> {
189                #[allow(clippy::useless_conversion)]
190                <::yew::virtual_dom::VNode as ::std::convert::From<_>>::from(#new_tokens)
191            }},
192        );
193    }
194}
195
196/// This trait represents a type that can be unfolded into multiple html nodes.
197pub trait ToNodeIterator {
198    /// Generate a token stream which produces a value that implements IntoIterator<Item=T> where T
199    /// is inferred by the compiler. The easiest way to achieve this is to call `.into()` on
200    /// each element. If the resulting iterator only ever yields a single item this function
201    /// should return None instead.
202    fn to_node_iterator_stream(&self) -> Option<TokenStream>;
203    /// Returns a boolean indicating whether the node can only ever unfold into 1 node
204    /// Same as calling `.to_node_iterator_stream().is_none()`,
205    /// but doesn't actually construct any token stream
206    fn is_singular(&self) -> bool;
207}
208
209impl ToNodeIterator for HtmlTree {
210    fn to_node_iterator_stream(&self) -> Option<TokenStream> {
211        match self {
212            Self::Block(block) => block.to_node_iterator_stream(),
213            // everything else is just a single node.
214            _ => None,
215        }
216    }
217
218    fn is_singular(&self) -> bool {
219        match self {
220            Self::Block(block) => block.is_singular(),
221            _ => true,
222        }
223    }
224}
225
226pub struct HtmlChildrenTree(pub Vec<HtmlTree>);
227
228impl HtmlChildrenTree {
229    pub fn new() -> Self {
230        Self(Vec::new())
231    }
232
233    pub fn parse_child(&mut self, input: ParseStream) -> syn::Result<()> {
234        self.0.push(input.parse()?);
235        Ok(())
236    }
237
238    pub fn is_empty(&self) -> bool {
239        self.0.is_empty()
240    }
241
242    // Check if each child represents a single node.
243    // This is the case when no expressions are used.
244    fn only_single_node_children(&self) -> bool {
245        self.0.iter().all(HtmlTree::is_singular)
246    }
247
248    pub fn to_build_vec_token_stream(&self) -> TokenStream {
249        let Self(children) = self;
250
251        if self.only_single_node_children() {
252            // optimize for the common case where all children are single nodes (only using literal
253            // html).
254            let children_into = children
255                .iter()
256                .map(|child| quote_spanned! {child.span()=> ::std::convert::Into::into(#child) });
257            return quote! {
258                [#(#children_into),*].to_vec()
259            };
260        }
261
262        let vec_ident = Ident::new("__yew_v", Span::mixed_site());
263        let add_children_streams = children.iter().map(|child| {
264            if let Some(node_iterator_stream) = child.to_node_iterator_stream() {
265                quote! {
266                    ::std::iter::Extend::extend(&mut #vec_ident, #node_iterator_stream);
267                }
268            } else {
269                quote_spanned! {child.span()=>
270                    #vec_ident.push(::std::convert::Into::into(#child));
271                }
272            }
273        });
274
275        quote! {
276            {
277                let mut #vec_ident = ::std::vec::Vec::new();
278                #(#add_children_streams)*
279                #vec_ident
280            }
281        }
282    }
283
284    fn parse_delimited(input: ParseStream) -> syn::Result<Self> {
285        let mut children = HtmlChildrenTree::new();
286
287        while !input.is_empty() {
288            children.parse_child(input)?;
289        }
290
291        Ok(children)
292    }
293
294    pub fn to_children_renderer_tokens(&self) -> Option<TokenStream> {
295        match self.0[..] {
296            [] => None,
297            [HtmlTree::Component(ref children)] => Some(quote! { #children }),
298            [HtmlTree::Element(ref children)] => Some(quote! { #children }),
299            [HtmlTree::Block(ref m)] => {
300                // We only want to process `{vnode}` and not `{for vnodes}`.
301                // This should be converted into a if let guard once https://github.com/rust-lang/rust/issues/51114 is stable.
302                // Or further nested once deref pattern (https://github.com/rust-lang/rust/issues/87121) is stable.
303                if let HtmlBlock {
304                    content: BlockContent::Node(children),
305                    ..
306                } = m.as_ref()
307                {
308                    Some(quote! { #children })
309                } else {
310                    Some(quote! { ::yew::html::ChildrenRenderer::new(#self) })
311                }
312            }
313            _ => Some(quote! { ::yew::html::ChildrenRenderer::new(#self) }),
314        }
315    }
316
317    pub fn to_vnode_tokens(&self) -> TokenStream {
318        match self.0[..] {
319            [] => quote! {::std::default::Default::default() },
320            [HtmlTree::Component(ref children)] => {
321                quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) }
322            }
323            [HtmlTree::Element(ref children)] => {
324                quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) }
325            }
326            [HtmlTree::Block(ref m)] => {
327                // We only want to process `{vnode}` and not `{for vnodes}`.
328                // This should be converted into a if let guard once https://github.com/rust-lang/rust/issues/51114 is stable.
329                // Or further nested once deref pattern (https://github.com/rust-lang/rust/issues/87121) is stable.
330                if let HtmlBlock {
331                    content: BlockContent::Node(children),
332                    ..
333                } = m.as_ref()
334                {
335                    quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) }
336                } else {
337                    quote! {
338                        ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(
339                            ::yew::html::ChildrenRenderer::new(#self)
340                        )
341                    }
342                }
343            }
344            _ => quote! {
345                ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(
346                    ::yew::html::ChildrenRenderer::new(#self)
347                )
348            },
349        }
350    }
351
352    pub fn size_hint(&self) -> Option<usize> {
353        self.only_single_node_children().then_some(self.0.len())
354    }
355
356    pub fn fully_keyed(&self) -> Option<bool> {
357        for child in self.0.iter() {
358            match child {
359                HtmlTree::Block(block) => {
360                    return if let BlockContent::Node(node) = &block.content {
361                        matches!(&**node, HtmlNode::Literal(_)).then_some(false)
362                    } else {
363                        None
364                    }
365                }
366                HtmlTree::Component(comp) => {
367                    if comp.props.props.special.key.is_none() {
368                        return Some(false);
369                    }
370                }
371                HtmlTree::List(list) => {
372                    if list.open.props.key.is_none() {
373                        return Some(false);
374                    }
375                }
376                HtmlTree::Element(element) => {
377                    if element.props.special.key.is_none() {
378                        return Some(false);
379                    }
380                }
381                HtmlTree::If(_) | HtmlTree::For(_) | HtmlTree::Empty => return Some(false),
382            }
383        }
384        Some(true)
385    }
386}
387
388impl ToTokens for HtmlChildrenTree {
389    fn to_tokens(&self, tokens: &mut TokenStream) {
390        tokens.extend(self.to_build_vec_token_stream());
391    }
392}
393
394pub struct HtmlRootBraced {
395    brace: token::Brace,
396    children: HtmlChildrenTree,
397}
398
399impl PeekValue<()> for HtmlRootBraced {
400    fn peek(cursor: Cursor) -> Option<()> {
401        cursor.group(Delimiter::Brace).map(|_| ())
402    }
403}
404
405impl Parse for HtmlRootBraced {
406    fn parse(input: ParseStream) -> syn::Result<Self> {
407        let content;
408        let brace = braced!(content in input);
409        let children = HtmlChildrenTree::parse_delimited(&content)?;
410
411        Ok(HtmlRootBraced { brace, children })
412    }
413}
414
415impl ToTokens for HtmlRootBraced {
416    fn to_tokens(&self, tokens: &mut TokenStream) {
417        let Self { brace, children } = self;
418
419        tokens.extend(quote_spanned! {brace.span.span()=>
420            {
421                ::yew::virtual_dom::VNode::VList(::std::rc::Rc::new(
422                    ::yew::virtual_dom::VList::with_children(#children, ::std::option::Option::None)
423                ))
424            }
425        });
426    }
427}