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