1use std::convert::TryFrom;
2use std::ops::{Deref, DerefMut};
3
4use proc_macro2::{Spacing, Span, TokenStream, TokenTree};
5use quote::{ToTokens, quote, quote_spanned};
6use syn::parse::{Parse, ParseBuffer, ParseStream};
7use syn::spanned::Spanned;
8use syn::token::Brace;
9use syn::{
10 Block, Expr, ExprBlock, ExprMacro, ExprPath, ExprRange, LitStr, Stmt, Token, braced,
11 parse_quote,
12};
13
14use crate::html_tree::HtmlDashedName;
15use crate::stringify::Stringify;
16
17#[derive(Copy, Clone)]
18pub enum PropDirective {
19 ApplyAsProperty(Token![~]),
20}
21
22pub enum PropLabel {
23 Static(HtmlDashedName),
24 Dynamic(Expr),
25}
26
27impl From<HtmlDashedName> for PropLabel {
28 fn from(value: HtmlDashedName) -> Self {
29 Self::Static(value)
30 }
31}
32
33impl From<LitStr> for PropLabel {
34 fn from(value: LitStr) -> Self {
35 Self::Dynamic(parse_quote! { #value })
36 }
37}
38
39impl TryFrom<PropLabel> for HtmlDashedName {
40 type Error = ();
41
42 fn try_from(value: PropLabel) -> Result<Self, Self::Error> {
43 use PropLabel::*;
44 match value {
45 Static(dashed_name) => Ok(dashed_name),
46 Dynamic(_) => Err(()),
47 }
48 }
49}
50
51impl<'a> TryFrom<&'a PropLabel> for &'a HtmlDashedName {
52 type Error = ();
53
54 fn try_from(value: &'a PropLabel) -> Result<Self, Self::Error> {
55 use PropLabel::*;
56 match value {
57 Static(dashed_name) => Ok(dashed_name),
58 Dynamic(_) => Err(()),
59 }
60 }
61}
62
63impl TryFrom<PropLabel> for String {
64 type Error = ();
65
66 fn try_from(value: PropLabel) -> Result<Self, Self::Error> {
67 HtmlDashedName::try_from(value).map(|dashed_name| dashed_name.to_string())
68 }
69}
70
71impl TryFrom<&PropLabel> for String {
72 type Error = ();
73
74 fn try_from(value: &PropLabel) -> Result<Self, Self::Error> {
75 <&HtmlDashedName>::try_from(value).map(|dashed_name| dashed_name.to_string())
76 }
77}
78
79impl PartialEq<PropLabel> for PropLabel {
80 fn eq(&self, other: &PropLabel) -> bool {
81 match (self, other) {
82 (Self::Static(l), Self::Static(r)) => l == r,
83 _ => false,
85 }
86 }
87}
88
89impl ToTokens for PropLabel {
90 fn to_tokens(&self, tokens: &mut TokenStream) {
91 match self {
92 Self::Static(name) => name.to_tokens(tokens),
93 Self::Dynamic(expr) => expr.to_tokens(tokens),
94 }
95 }
96}
97
98pub struct Prop {
99 pub directive: Option<PropDirective>,
100 pub label: PropLabel,
101 pub value: Expr,
103}
104
105impl Parse for Prop {
106 fn parse(input: ParseStream) -> syn::Result<Self> {
107 let directive = input
108 .parse::<Token![~]>()
109 .map(PropDirective::ApplyAsProperty)
110 .ok();
111 if input.peek(Brace) {
112 Self::parse_shorthand_or_expr_dynamic_prop_assignment(input, directive)
113 } else if input.peek(LitStr) {
114 Self::parse_literal_dynamic_prop_assignment(input, directive)
115 } else {
116 Self::parse_prop_assignment(input, directive)
117 }
118 }
119}
120
121impl Prop {
123 fn parse_shorthand_or_expr_dynamic_prop_assignment(
129 input: ParseStream,
130 directive: Option<PropDirective>,
131 ) -> syn::Result<Self> {
132 let value;
133 let _brace = braced!(value in input);
134 let expr = value.parse::<Expr>()?;
135
136 if input.peek(Token![=]) {
138 input.parse::<Token![=]>().unwrap();
139 let value = parse_prop_value(input)?;
140 return Ok(Self {
141 label: PropLabel::Dynamic(expr),
142 value,
143 directive,
144 });
145 }
146 let label = if let Expr::Path(ExprPath {
149 ref attrs,
150 qself: None,
151 ref path,
152 }) = expr
153 {
154 if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) {
155 Ok(HtmlDashedName::from(ident.clone()))
156 } else {
157 Err(syn::Error::new_spanned(
158 path,
159 "only simple identifiers are allowed in the shorthand property syntax",
160 ))
161 }
162 } else {
163 return Err(syn::Error::new_spanned(
164 expr,
165 "missing label for property value. If trying to use the shorthand property \
166 syntax, only identifiers may be used",
167 ));
168 }?;
169
170 Ok(Self {
171 label: label.into(),
172 value: expr,
173 directive,
174 })
175 }
176
177 fn parse_literal_dynamic_prop_assignment(
179 input: ParseStream,
180 directive: Option<PropDirective>,
181 ) -> syn::Result<Self> {
182 let label = input.parse::<LitStr>()?;
183 let equals = input.parse::<Token![=]>().map_err(|_| {
184 let display = label.stringify();
185 syn::Error::new_spanned(
186 &label,
187 format!(
188 "`{display}` doesn't have a value. (hint: set the value to `true` or `false` \
189 for boolean attributes)"
190 ),
191 )
192 })?;
193 if input.is_empty() {
194 return Err(syn::Error::new_spanned(
195 equals,
196 "expected an expression following this equals sign",
197 ));
198 }
199
200 let value = parse_prop_value(input)?;
201 Ok(Self {
202 label: label.into(),
203 value,
204 directive,
205 })
206 }
207
208 fn parse_prop_assignment(
210 input: ParseStream,
211 directive: Option<PropDirective>,
212 ) -> syn::Result<Self> {
213 let label = input.parse::<HtmlDashedName>()?;
214 let equals = input.parse::<Token![=]>().map_err(|_| {
215 syn::Error::new_spanned(
216 &label,
217 format!(
218 "`{label}` doesn't have a value. (hint: set the value to `true` or `false` \
219 for boolean attributes)"
220 ),
221 )
222 })?;
223 if input.is_empty() {
224 return Err(syn::Error::new_spanned(
225 equals,
226 "expected an expression following this equals sign",
227 ));
228 }
229
230 let value = parse_prop_value(input)?;
231 Ok(Self {
232 label: label.into(),
233 value,
234 directive,
235 })
236 }
237}
238
239fn parse_prop_value(input: &ParseBuffer) -> syn::Result<Expr> {
240 if input.peek(Brace) {
241 strip_braces(input.parse()?)
242 } else {
243 let expr = match range_expression_peek(input) {
244 Some(ExprRange {
245 start: Some(start), ..
246 }) => {
247 advance_until_next_dot2(input)?;
250 *start
251 }
252 _ => input.parse()?,
253 };
254
255 match &expr {
256 Expr::Lit(_) => Ok(expr),
257 ref exp => Err(syn::Error::new_spanned(
258 &expr,
259 format!(
260 "the property value must be either a literal or enclosed in braces. Consider \
261 adding braces around your expression.: {exp:#?}"
262 ),
263 )),
264 }
265 }
266}
267
268fn strip_braces(block: ExprBlock) -> syn::Result<Expr> {
269 match block {
270 ExprBlock {
271 block: Block { mut stmts, .. },
272 ..
273 } if stmts.len() == 1 => {
274 let stmt = stmts.remove(0);
275 match stmt {
276 Stmt::Expr(expr, None) => Ok(expr),
277 Stmt::Macro(mac) => Ok(Expr::Macro(ExprMacro {
278 attrs: vec![],
279 mac: mac.mac,
280 })),
281 Stmt::Item(syn::Item::Macro(mac))
283 if mac.ident.is_none() && mac.semi_token.is_none() =>
284 {
285 Ok(Expr::Macro(syn::ExprMacro {
286 attrs: mac.attrs,
287 mac: mac.mac,
288 }))
289 }
290 Stmt::Expr(_, Some(semi)) => Err(syn::Error::new_spanned(
291 semi,
292 "only an expression may be assigned as a property. Consider removing this \
293 semicolon",
294 )),
295 _ => Err(syn::Error::new_spanned(
296 stmt,
297 "only an expression may be assigned as a property",
298 )),
299 }
300 }
301 block => Ok(Expr::Block(block)),
302 }
303}
304
305fn range_expression_peek(input: &ParseBuffer) -> Option<ExprRange> {
307 match input.fork().parse::<Expr>().ok()? {
308 Expr::Range(range) => Some(range),
309 _ => None,
310 }
311}
312
313fn advance_until_next_dot2(input: &ParseBuffer) -> syn::Result<()> {
314 input.step(|cursor| {
315 let mut rest = *cursor;
316 let mut first_dot = None;
317 while let Some((tt, next)) = rest.token_tree() {
318 match &tt {
319 TokenTree::Punct(punct) if punct.as_char() == '.' => {
320 if let Some(first_dot) = first_dot {
321 return Ok(((), first_dot));
322 } else {
323 first_dot = if punct.spacing() == Spacing::Joint {
325 Some(rest)
326 } else {
327 None
328 };
329 }
330 }
331 _ => {
332 first_dot = None;
333 }
334 }
335 rest = next;
336 }
337 Err(cursor.error("no `..` found in expression"))
338 })
339}
340
341pub struct PropList(Vec<Prop>);
348impl PropList {
349 pub fn new(props: Vec<Prop>) -> Self {
352 Self(props)
353 }
354
355 fn position(&self, key: &str) -> Option<usize> {
356 self.0.iter().position(
357 |it| matches!(String::try_from(&it.label), Ok(dashed_name) if dashed_name == key),
358 )
359 }
360
361 pub fn get_by_label(&self, key: &str) -> Option<&Prop> {
363 self.0
364 .iter()
365 .find(|it| matches!(String::try_from(&it.label), Ok(dashed_name) if dashed_name == key))
366 }
367
368 pub fn pop(&mut self, key: &str) -> Option<Prop> {
370 self.position(key).map(|i| self.0.remove(i))
371 }
372
373 pub fn pop_unique(&mut self, key: &str) -> syn::Result<Option<Prop>> {
375 let prop = self.pop(key);
376 if prop.is_some() {
377 if let Some(other_prop) = self.get_by_label(key) {
378 return Err(syn::Error::new_spanned(
379 &other_prop.label,
380 format!("`{key}` can only be specified once"),
381 ));
382 }
383 }
384
385 Ok(prop)
386 }
387
388 pub fn into_vec(self) -> Vec<Prop> {
390 self.0
391 }
392
393 fn iter_duplicates(&self) -> impl Iterator<Item = &Prop> {
395 self.0.windows(2).filter_map(|pair| {
396 let (a, b) = (&pair[0], &pair[1]);
397
398 if a.label == b.label { Some(b) } else { None }
399 })
400 }
401
402 pub fn drain_filter(&mut self, filter: impl FnMut(&Prop) -> bool) -> Self {
404 let (drained, others) = self.0.drain(..).partition(filter);
405 self.0 = others;
406 Self(drained)
407 }
408
409 pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> {
412 crate::join_errors(self.0.iter().map(f).filter_map(Result::err))
413 }
414
415 pub fn check_no_duplicates(&self) -> syn::Result<()> {
417 crate::join_errors(self.iter_duplicates().map(|prop| {
418 syn::Error::new_spanned(
419 &prop.label,
420 format!(
421 "`{}` can only be specified once but is given here again",
422 String::try_from(&prop.label).unwrap()
423 ),
424 )
425 }))
426 }
427}
428impl Parse for PropList {
429 fn parse(input: ParseStream) -> syn::Result<Self> {
430 let mut props: Vec<Prop> = Vec::new();
431 while !input.is_empty() && !input.peek(Token![..]) {
433 props.push(input.parse()?);
434 }
435
436 Ok(Self::new(props))
437 }
438}
439impl Deref for PropList {
440 type Target = [Prop];
441
442 fn deref(&self) -> &Self::Target {
443 &self.0
444 }
445}
446
447#[derive(Default)]
448pub struct SpecialProps {
449 pub node_ref: Option<Prop>,
450 pub key: Option<Prop>,
451}
452impl SpecialProps {
453 const KEY_LABEL: &'static str = "key";
454 const REF_LABEL: &'static str = "ref";
455
456 fn pop_from(props: &mut PropList) -> syn::Result<Self> {
457 let node_ref = props.pop_unique(Self::REF_LABEL)?;
458 let key = props.pop_unique(Self::KEY_LABEL)?;
459 Ok(Self { node_ref, key })
460 }
461
462 fn iter(&self) -> impl Iterator<Item = &Prop> {
463 self.node_ref.as_ref().into_iter().chain(self.key.as_ref())
464 }
465
466 pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> {
469 crate::join_errors(self.iter().map(f).filter_map(Result::err))
470 }
471
472 pub fn wrap_node_ref_attr(&self) -> TokenStream {
473 self.node_ref
474 .as_ref()
475 .map(|attr| {
476 let value = &attr.value;
477 quote_spanned! {value.span().resolved_at(Span::call_site())=>
478 ::yew::html::IntoPropValue::<::yew::html::NodeRef>
479 ::into_prop_value(#value)
480 }
481 })
482 .unwrap_or(quote! { ::std::default::Default::default() })
483 }
484
485 pub fn wrap_key_attr(&self) -> TokenStream {
486 self.key
487 .as_ref()
488 .map(|attr| {
489 let value = attr.value.optimize_literals();
490 quote_spanned! {value.span().resolved_at(Span::call_site())=>
491 ::std::option::Option::Some(
492 ::std::convert::Into::<::yew::virtual_dom::Key>::into(#value)
493 )
494 }
495 })
496 .unwrap_or(quote! { ::std::option::Option::None })
497 }
498}
499
500pub struct Props {
501 pub special: SpecialProps,
502 pub prop_list: PropList,
503}
504impl Parse for Props {
505 fn parse(input: ParseStream) -> syn::Result<Self> {
506 Self::try_from(input.parse::<PropList>()?)
507 }
508}
509impl Deref for Props {
510 type Target = PropList;
511
512 fn deref(&self) -> &Self::Target {
513 &self.prop_list
514 }
515}
516impl DerefMut for Props {
517 fn deref_mut(&mut self) -> &mut Self::Target {
518 &mut self.prop_list
519 }
520}
521
522impl TryFrom<PropList> for Props {
523 type Error = syn::Error;
524
525 fn try_from(mut prop_list: PropList) -> Result<Self, Self::Error> {
526 let special = SpecialProps::pop_from(&mut prop_list)?;
527 Ok(Self { special, prop_list })
528 }
529}