Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/props/
prop.rs

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            // NOTE: Dynamic props may repeat
84            _ => 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    /// Punctuation between `label` and `value`.
102    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
121/// Helpers for parsing props
122impl Prop {
123    /// Parse a prop using the shorthand syntax `{value}`, short for `value={value}`,
124    /// or using the `{label}={value}` dynamic label syntax.
125    ///
126    /// Shorthand syntax only allows for labels with no hyphens,
127    /// as it would otherwise create an ambiguity in the syntax.
128    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        // dynamic here
137        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        // otherwise, shorthand
147
148        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    /// Parse a prop of the form `"label"={value}`
178    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    /// Parse a prop of the form `label={value}`
209    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                // If a range expression is seen, treat the left-side expression as the value
248                // and leave the right-side expression to be parsed as a base expression
249                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                // See issue #2267, we want to parse macro invocations as expressions
282                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
305// Without advancing cursor, returns the range expression at the current cursor position if any
306fn 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                        // Only consider dot as potential first if there is no spacing after it
324                        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
341/// List of props sorted in alphabetical order*.
342///
343/// \*The "children" prop always comes last to match the behaviour of the `Properties` derive macro.
344///
345/// The list may contain multiple props with the same label.
346/// Use `check_no_duplicates` to ensure that there are no duplicates.
347pub struct PropList(Vec<Prop>);
348impl PropList {
349    /// Create a new `SortedPropList` from a vector of props.
350    /// The given `props` doesn't need to be sorted.
351    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    /// Get the first prop with the given key.
362    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    /// Pop the first prop with the given key.
369    pub fn pop(&mut self, key: &str) -> Option<Prop> {
370        self.position(key).map(|i| self.0.remove(i))
371    }
372
373    /// Pop the prop with the given key and error if there are multiple ones.
374    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    /// Turn the props into a vector of `Prop`.
389    pub fn into_vec(self) -> Vec<Prop> {
390        self.0
391    }
392
393    /// Iterate over all duplicate props in order of appearance.
394    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    /// Remove and return all props for which `filter` returns `true`.
403    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    /// Run the given function for all props and aggregate the errors.
410    /// If there's at least one error, the result will be `Result::Err`.
411    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    /// Return an error for all duplicate props.
416    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        // Stop parsing props if a base expression preceded by `..` is reached
432        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    /// Run the given function for all props and aggregate the errors.
467    /// If there's at least one error, the result will be `Result::Err`.
468    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}