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_list.rs

1use quote::{quote, quote_spanned, ToTokens};
2use syn::buffer::Cursor;
3use syn::parse::{Parse, ParseStream};
4use syn::spanned::Spanned;
5use syn::Expr;
6
7use super::html_dashed_name::HtmlDashedName;
8use super::{HtmlChildrenTree, TagTokens};
9use crate::props::Prop;
10use crate::{Peek, PeekValue};
11
12pub struct HtmlList {
13    pub open: HtmlListOpen,
14    pub children: HtmlChildrenTree,
15    close: HtmlListClose,
16}
17
18impl PeekValue<()> for HtmlList {
19    fn peek(cursor: Cursor) -> Option<()> {
20        HtmlListOpen::peek(cursor)
21            .or_else(|| HtmlListClose::peek(cursor))
22            .map(|_| ())
23    }
24}
25
26impl Parse for HtmlList {
27    fn parse(input: ParseStream) -> syn::Result<Self> {
28        if HtmlListClose::peek(input.cursor()).is_some() {
29            return match input.parse::<HtmlListClose>() {
30                Ok(close) => Err(syn::Error::new_spanned(
31                    close.to_spanned(),
32                    "this closing fragment has no corresponding opening fragment",
33                )),
34                Err(err) => Err(err),
35            };
36        }
37
38        let open = input.parse::<HtmlListOpen>()?;
39        let mut children = HtmlChildrenTree::new();
40        while HtmlListClose::peek(input.cursor()).is_none() {
41            children.parse_child(input)?;
42            if input.is_empty() {
43                return Err(syn::Error::new_spanned(
44                    open.to_spanned(),
45                    "this opening fragment has no corresponding closing fragment",
46                ));
47            }
48        }
49
50        let close = input.parse::<HtmlListClose>()?;
51
52        Ok(Self {
53            open,
54            children,
55            close,
56        })
57    }
58}
59
60impl ToTokens for HtmlList {
61    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
62        let Self {
63            open,
64            children,
65            close,
66        } = &self;
67
68        let key = if let Some(key) = &open.props.key {
69            quote_spanned! {key.span()=> ::std::option::Option::Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#key))}
70        } else {
71            quote! { ::std::option::Option::None }
72        };
73
74        let span = {
75            let open = open.to_spanned();
76            let close = close.to_spanned();
77            quote! { #open #close }
78        }
79        .span();
80
81        tokens.extend(match children.fully_keyed() {
82            Some(true) => quote_spanned!{span=>
83                ::yew::virtual_dom::VList::__macro_new(#children, #key, ::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed)
84            },
85            Some(false) => quote_spanned!{span=>
86                ::yew::virtual_dom::VList::__macro_new(#children, #key, ::yew::virtual_dom::FullyKeyedState::KnownMissingKeys)
87            },
88            None => quote_spanned!{span=>
89                ::yew::virtual_dom::VList::with_children(#children, #key)
90            }
91        });
92    }
93}
94
95pub struct HtmlListOpen {
96    tag: TagTokens,
97    pub props: HtmlListProps,
98}
99impl HtmlListOpen {
100    fn to_spanned(&self) -> impl ToTokens {
101        self.tag.to_spanned()
102    }
103}
104
105impl PeekValue<()> for HtmlListOpen {
106    fn peek(cursor: Cursor) -> Option<()> {
107        let (punct, cursor) = cursor.punct()?;
108        if punct.as_char() != '<' {
109            return None;
110        }
111        // make sure it's either a property (key=value) or it's immediately closed
112        if let Some((_, cursor)) = HtmlDashedName::peek(cursor) {
113            let (punct, _) = cursor.punct()?;
114            (punct.as_char() == '=' || punct.as_char() == '?').then_some(())
115        } else {
116            let (punct, _) = cursor.punct()?;
117            (punct.as_char() == '>').then_some(())
118        }
119    }
120}
121
122impl Parse for HtmlListOpen {
123    fn parse(input: ParseStream) -> syn::Result<Self> {
124        TagTokens::parse_start_content(input, |input, tag| {
125            let props = input.parse()?;
126            Ok(Self { tag, props })
127        })
128    }
129}
130
131pub struct HtmlListProps {
132    pub key: Option<Expr>,
133}
134impl Parse for HtmlListProps {
135    fn parse(input: ParseStream) -> syn::Result<Self> {
136        let key = if input.is_empty() {
137            None
138        } else {
139            let prop: Prop = input.parse()?;
140            if !input.is_empty() {
141                return Err(input.error("only a single `key` prop is allowed on a fragment"));
142            }
143
144            if prop.label.to_ascii_lowercase_string() != "key" {
145                return Err(syn::Error::new_spanned(
146                    prop.label,
147                    "fragments only accept the `key` prop",
148                ));
149            }
150
151            Some(prop.value)
152        };
153
154        Ok(Self { key })
155    }
156}
157
158struct HtmlListClose(TagTokens);
159impl HtmlListClose {
160    fn to_spanned(&self) -> impl ToTokens {
161        self.0.to_spanned()
162    }
163}
164impl PeekValue<()> for HtmlListClose {
165    fn peek(cursor: Cursor) -> Option<()> {
166        let (punct, cursor) = cursor.punct()?;
167        if punct.as_char() != '<' {
168            return None;
169        }
170        let (punct, cursor) = cursor.punct()?;
171        if punct.as_char() != '/' {
172            return None;
173        }
174
175        let (punct, _) = cursor.punct()?;
176        (punct.as_char() == '>').then_some(())
177    }
178}
179impl Parse for HtmlListClose {
180    fn parse(input: ParseStream) -> syn::Result<Self> {
181        TagTokens::parse_end_content(input, |input, tag| {
182            if !input.is_empty() {
183                Err(input.error("unexpected content in list close"))
184            } else {
185                Ok(Self(tag))
186            }
187        })
188    }
189}