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_if;
16mod html_iterable;
17mod html_list;
18mod html_node;
19mod lint;
20mod tag;
21
22use html_block::HtmlBlock;
23use html_component::HtmlComponent;
24pub use html_dashed_name::HtmlDashedName;
25use html_element::HtmlElement;
26use html_if::HtmlIf;
27use html_iterable::HtmlIterable;
28use html_list::HtmlList;
29use html_node::HtmlNode;
30use tag::TagTokens;
31
32use self::html_block::BlockContent;
33
34pub enum HtmlType {
35    Block,
36    Component,
37    List,
38    Element,
39    If,
40    Empty,
41}
42
43pub enum HtmlTree {
44    Block(Box<HtmlBlock>),
45    Component(Box<HtmlComponent>),
46    List(Box<HtmlList>),
47    Element(Box<HtmlElement>),
48    If(Box<HtmlIf>),
49    Empty,
50}
51
52impl Parse for HtmlTree {
53    fn parse(input: ParseStream) -> syn::Result<Self> {
54        let html_type = Self::peek_html_type(input)
55            .ok_or_else(|| input.error("expected a valid html element"))?;
56        let html_tree = match html_type {
57            HtmlType::Empty => HtmlTree::Empty,
58            HtmlType::Component => HtmlTree::Component(Box::new(input.parse()?)),
59            HtmlType::Element => HtmlTree::Element(Box::new(input.parse()?)),
60            HtmlType::Block => HtmlTree::Block(Box::new(input.parse()?)),
61            HtmlType::List => HtmlTree::List(Box::new(input.parse()?)),
62            HtmlType::If => HtmlTree::If(Box::new(input.parse()?)),
63        };
64        Ok(html_tree)
65    }
66}
67
68impl HtmlTree {
69    /// Determine the [`HtmlType`] before actually parsing it.
70    /// Even though this method accepts a [`ParseStream`], it is forked and the original stream is
71    /// not modified. Once a certain `HtmlType` can be deduced for certain, the function eagerly
72    /// returns with the appropriate type. If invalid html tag, returns `None`.
73    fn peek_html_type(input: ParseStream) -> Option<HtmlType> {
74        let input = input.fork(); // do not modify original ParseStream
75
76        if input.is_empty() {
77            Some(HtmlType::Empty)
78        } else if input
79            .cursor()
80            .group(proc_macro2::Delimiter::Brace)
81            .is_some()
82        {
83            Some(HtmlType::Block)
84        } else if HtmlIf::peek(input.cursor()).is_some() {
85            Some(HtmlType::If)
86        } else if input.peek(Token![<]) {
87            let _lt: Token![<] = input.parse().ok()?;
88
89            // eat '/' character for unmatched closing tag
90            let _slash: Option<Token![/]> = input.parse().ok();
91
92            if input.peek(Token![>]) {
93                Some(HtmlType::List)
94            } else if input.peek(Token![@]) {
95                Some(HtmlType::Element) // dynamic element
96            } else if input.peek(Token![::]) {
97                Some(HtmlType::Component)
98            } else if input.peek(Ident::peek_any) {
99                let ident = Ident::parse_any(&input).ok()?;
100                let ident_str = ident.to_string();
101
102                if input.peek(Token![=]) || (input.peek(Token![?]) && input.peek2(Token![=])) {
103                    Some(HtmlType::List)
104                } else if ident_str.chars().next().unwrap().is_ascii_uppercase()
105                    || input.peek(Token![::])
106                    || is_ide_completion() && ident_str.chars().any(|c| c.is_ascii_uppercase())
107                {
108                    Some(HtmlType::Component)
109                } else {
110                    Some(HtmlType::Element)
111                }
112            } else {
113                None
114            }
115        } else {
116            None
117        }
118    }
119}
120
121impl ToTokens for HtmlTree {
122    fn to_tokens(&self, tokens: &mut TokenStream) {
123        lint::lint_all(self);
124        match self {
125            HtmlTree::Empty => tokens.extend(quote! {
126                <::yew::virtual_dom::VNode as ::std::default::Default>::default()
127            }),
128            HtmlTree::Component(comp) => comp.to_tokens(tokens),
129            HtmlTree::Element(tag) => tag.to_tokens(tokens),
130            HtmlTree::List(list) => list.to_tokens(tokens),
131            HtmlTree::Block(block) => block.to_tokens(tokens),
132            HtmlTree::If(block) => block.to_tokens(tokens),
133        }
134    }
135}
136
137pub enum HtmlRoot {
138    Tree(HtmlTree),
139    Iterable(Box<HtmlIterable>),
140    Node(Box<HtmlNode>),
141}
142
143impl Parse for HtmlRoot {
144    fn parse(input: ParseStream) -> syn::Result<Self> {
145        let html_root = if HtmlTree::peek_html_type(input).is_some() {
146            Self::Tree(input.parse()?)
147        } else if HtmlIterable::peek(input.cursor()).is_some() {
148            Self::Iterable(Box::new(input.parse()?))
149        } else {
150            Self::Node(Box::new(input.parse()?))
151        };
152
153        if !input.is_empty() {
154            let stream: TokenStream = input.parse()?;
155            Err(syn::Error::new_spanned(
156                stream,
157                "only one root html element is allowed (hint: you can wrap multiple html elements \
158                 in a fragment `<></>`)",
159            ))
160        } else {
161            Ok(html_root)
162        }
163    }
164}
165
166impl ToTokens for HtmlRoot {
167    fn to_tokens(&self, tokens: &mut TokenStream) {
168        match self {
169            Self::Tree(tree) => tree.to_tokens(tokens),
170            Self::Node(node) => node.to_tokens(tokens),
171            Self::Iterable(iterable) => iterable.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}
204
205impl ToNodeIterator for HtmlTree {
206    fn to_node_iterator_stream(&self) -> Option<TokenStream> {
207        match self {
208            HtmlTree::Block(block) => block.to_node_iterator_stream(),
209            // everything else is just a single node.
210            _ => None,
211        }
212    }
213}
214
215pub struct HtmlChildrenTree(pub Vec<HtmlTree>);
216
217impl HtmlChildrenTree {
218    pub fn new() -> Self {
219        Self(Vec::new())
220    }
221
222    pub fn parse_child(&mut self, input: ParseStream) -> syn::Result<()> {
223        self.0.push(input.parse()?);
224        Ok(())
225    }
226
227    pub fn is_empty(&self) -> bool {
228        self.0.is_empty()
229    }
230
231    // Check if each child represents a single node.
232    // This is the case when no expressions are used.
233    fn only_single_node_children(&self) -> bool {
234        self.0
235            .iter()
236            .map(ToNodeIterator::to_node_iterator_stream)
237            .all(|s| s.is_none())
238    }
239
240    pub fn to_build_vec_token_stream(&self) -> TokenStream {
241        let Self(children) = self;
242
243        if self.only_single_node_children() {
244            // optimize for the common case where all children are single nodes (only using literal
245            // html).
246            let children_into = children
247                .iter()
248                .map(|child| quote_spanned! {child.span()=> ::std::convert::Into::into(#child) });
249            return quote! {
250                ::std::vec![#(#children_into),*]
251            };
252        }
253
254        let vec_ident = Ident::new("__yew_v", Span::mixed_site());
255        let add_children_streams = children.iter().map(|child| {
256            if let Some(node_iterator_stream) = child.to_node_iterator_stream() {
257                quote! {
258                    ::std::iter::Extend::extend(&mut #vec_ident, #node_iterator_stream);
259                }
260            } else {
261                quote_spanned! {child.span()=>
262                    #vec_ident.push(::std::convert::Into::into(#child));
263                }
264            }
265        });
266
267        quote! {
268            {
269                let mut #vec_ident = ::std::vec::Vec::new();
270                #(#add_children_streams)*
271                #vec_ident
272            }
273        }
274    }
275
276    fn parse_delimited(input: ParseStream) -> syn::Result<Self> {
277        let mut children = HtmlChildrenTree::new();
278
279        while !input.is_empty() {
280            children.parse_child(input)?;
281        }
282
283        Ok(children)
284    }
285
286    pub fn to_children_renderer_tokens(&self) -> Option<TokenStream> {
287        match self.0[..] {
288            [] => None,
289            [HtmlTree::Component(ref children)] => Some(quote! { #children }),
290            [HtmlTree::Element(ref children)] => Some(quote! { #children }),
291            [HtmlTree::Block(ref m)] => {
292                // We only want to process `{vnode}` and not `{for vnodes}`.
293                // This should be converted into a if let guard once https://github.com/rust-lang/rust/issues/51114 is stable.
294                // Or further nested once deref pattern (https://github.com/rust-lang/rust/issues/87121) is stable.
295                if let HtmlBlock {
296                    content: BlockContent::Node(children),
297                    ..
298                } = m.as_ref()
299                {
300                    Some(quote! { #children })
301                } else {
302                    Some(quote! { ::yew::html::ChildrenRenderer::new(#self) })
303                }
304            }
305            _ => Some(quote! { ::yew::html::ChildrenRenderer::new(#self) }),
306        }
307    }
308
309    pub fn to_vnode_tokens(&self) -> TokenStream {
310        match self.0[..] {
311            [] => quote! {::std::default::Default::default() },
312            [HtmlTree::Component(ref children)] => {
313                quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) }
314            }
315            [HtmlTree::Element(ref children)] => {
316                quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) }
317            }
318            [HtmlTree::Block(ref m)] => {
319                // We only want to process `{vnode}` and not `{for vnodes}`.
320                // This should be converted into a if let guard once https://github.com/rust-lang/rust/issues/51114 is stable.
321                // Or further nested once deref pattern (https://github.com/rust-lang/rust/issues/87121) is stable.
322                if let HtmlBlock {
323                    content: BlockContent::Node(children),
324                    ..
325                } = m.as_ref()
326                {
327                    quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) }
328                } else {
329                    quote! {
330                        ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(
331                            ::yew::html::ChildrenRenderer::new(#self)
332                        )
333                    }
334                }
335            }
336            _ => quote! {
337                ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(
338                    ::yew::html::ChildrenRenderer::new(#self)
339                )
340            },
341        }
342    }
343}
344
345impl ToTokens for HtmlChildrenTree {
346    fn to_tokens(&self, tokens: &mut TokenStream) {
347        tokens.extend(self.to_build_vec_token_stream());
348    }
349}
350
351pub struct HtmlRootBraced {
352    brace: token::Brace,
353    children: HtmlChildrenTree,
354}
355
356impl PeekValue<()> for HtmlRootBraced {
357    fn peek(cursor: Cursor) -> Option<()> {
358        cursor.group(Delimiter::Brace).map(|_| ())
359    }
360}
361
362impl Parse for HtmlRootBraced {
363    fn parse(input: ParseStream) -> syn::Result<Self> {
364        let content;
365        let brace = braced!(content in input);
366        let children = HtmlChildrenTree::parse_delimited(&content)?;
367
368        Ok(HtmlRootBraced { brace, children })
369    }
370}
371
372impl ToTokens for HtmlRootBraced {
373    fn to_tokens(&self, tokens: &mut TokenStream) {
374        let Self { brace, children } = self;
375
376        tokens.extend(quote_spanned! {brace.span.span()=>
377            {
378                ::yew::virtual_dom::VNode::VList(::std::rc::Rc::new(
379                    ::yew::virtual_dom::VList::with_children(#children, ::std::option::Option::None)
380                ))
381            }
382        });
383    }
384}