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 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 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}