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    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 spanned = {
75            let open = open.to_spanned();
76            let close = close.to_spanned();
77            quote! { #open #close }
78        };
79
80        tokens.extend(quote_spanned! {spanned.span()=>
81            ::yew::virtual_dom::VNode::VList(::std::rc::Rc::new(
82                ::yew::virtual_dom::VList::with_children(#children, #key)
83            ))
84        });
85    }
86}
87
88struct HtmlListOpen {
89    tag: TagTokens,
90    props: HtmlListProps,
91}
92impl HtmlListOpen {
93    fn to_spanned(&self) -> impl ToTokens {
94        self.tag.to_spanned()
95    }
96}
97
98impl PeekValue<()> for HtmlListOpen {
99    fn peek(cursor: Cursor) -> Option<()> {
100        let (punct, cursor) = cursor.punct()?;
101        if punct.as_char() != '<' {
102            return None;
103        }
104        // make sure it's either a property (key=value) or it's immediately closed
105        if let Some((_, cursor)) = HtmlDashedName::peek(cursor) {
106            let (punct, _) = cursor.punct()?;
107            (punct.as_char() == '=' || punct.as_char() == '?').then_some(())
108        } else {
109            let (punct, _) = cursor.punct()?;
110            (punct.as_char() == '>').then_some(())
111        }
112    }
113}
114
115impl Parse for HtmlListOpen {
116    fn parse(input: ParseStream) -> syn::Result<Self> {
117        TagTokens::parse_start_content(input, |input, tag| {
118            let props = input.parse()?;
119            Ok(Self { tag, props })
120        })
121    }
122}
123
124struct HtmlListProps {
125    key: Option<Expr>,
126}
127impl Parse for HtmlListProps {
128    fn parse(input: ParseStream) -> syn::Result<Self> {
129        let key = if input.is_empty() {
130            None
131        } else {
132            let prop: Prop = input.parse()?;
133            if !input.is_empty() {
134                return Err(input.error("only a single `key` prop is allowed on a fragment"));
135            }
136
137            if prop.label.to_ascii_lowercase_string() != "key" {
138                return Err(syn::Error::new_spanned(
139                    prop.label,
140                    "fragments only accept the `key` prop",
141                ));
142            }
143
144            Some(prop.value)
145        };
146
147        Ok(Self { key })
148    }
149}
150
151struct HtmlListClose(TagTokens);
152impl HtmlListClose {
153    fn to_spanned(&self) -> impl ToTokens {
154        self.0.to_spanned()
155    }
156}
157impl PeekValue<()> for HtmlListClose {
158    fn peek(cursor: Cursor) -> Option<()> {
159        let (punct, cursor) = cursor.punct()?;
160        if punct.as_char() != '<' {
161            return None;
162        }
163        let (punct, cursor) = cursor.punct()?;
164        if punct.as_char() != '/' {
165            return None;
166        }
167
168        let (punct, _) = cursor.punct()?;
169        (punct.as_char() == '>').then_some(())
170    }
171}
172impl Parse for HtmlListClose {
173    fn parse(input: ParseStream) -> syn::Result<Self> {
174        TagTokens::parse_end_content(input, |input, tag| {
175            if !input.is_empty() {
176                Err(input.error("unexpected content in list close"))
177            } else {
178                Ok(Self(tag))
179            }
180        })
181    }
182}