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::{quote, quote_spanned};
6use syn::parse::{Parse, ParseBuffer, ParseStream};
7use syn::spanned::Spanned;
8use syn::token::Brace;
9use syn::{braced, Block, Expr, ExprBlock, ExprMacro, ExprPath, ExprRange, Stmt, Token};
10
11use crate::html_tree::HtmlDashedName;
12use crate::stringify::Stringify;
13
14#[derive(Copy, Clone)]
15pub enum PropDirective {
16    ApplyAsProperty(Token![~]),
17}
18
19pub struct Prop {
20    pub directive: Option<PropDirective>,
21    pub label: HtmlDashedName,
22    /// Punctuation between `label` and `value`.
23    pub value: Expr,
24}
25
26impl Parse for Prop {
27    fn parse(input: ParseStream) -> syn::Result<Self> {
28        let directive = input
29            .parse::<Token![~]>()
30            .map(PropDirective::ApplyAsProperty)
31            .ok();
32        if input.peek(Brace) {
33            Self::parse_shorthand_prop_assignment(input, directive)
34        } else {
35            Self::parse_prop_assignment(input, directive)
36        }
37    }
38}
39
40/// Helpers for parsing props
41impl Prop {
42    /// Parse a prop using the shorthand syntax `{value}`, short for `value={value}`
43    /// This only allows for labels with no hyphens, as it would otherwise create
44    /// an ambiguity in the syntax
45    fn parse_shorthand_prop_assignment(
46        input: ParseStream,
47        directive: Option<PropDirective>,
48    ) -> syn::Result<Self> {
49        let value;
50        let _brace = braced!(value in input);
51        let expr = value.parse::<Expr>()?;
52        let label = if let Expr::Path(ExprPath {
53            ref attrs,
54            qself: None,
55            ref path,
56        }) = expr
57        {
58            if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) {
59                Ok(HtmlDashedName::from(ident.clone()))
60            } else {
61                Err(syn::Error::new_spanned(
62                    path,
63                    "only simple identifiers are allowed in the shorthand property syntax",
64                ))
65            }
66        } else {
67            return Err(syn::Error::new_spanned(
68                expr,
69                "missing label for property value. If trying to use the shorthand property \
70                 syntax, only identifiers may be used",
71            ));
72        }?;
73
74        Ok(Self {
75            label,
76            value: expr,
77            directive,
78        })
79    }
80
81    /// Parse a prop of the form `label={value}`
82    fn parse_prop_assignment(
83        input: ParseStream,
84        directive: Option<PropDirective>,
85    ) -> syn::Result<Self> {
86        let label = input.parse::<HtmlDashedName>()?;
87        let equals = input.parse::<Token![=]>().map_err(|_| {
88            syn::Error::new_spanned(
89                &label,
90                format!(
91                    "`{label}` doesn't have a value. (hint: set the value to `true` or `false` \
92                     for boolean attributes)"
93                ),
94            )
95        })?;
96        if input.is_empty() {
97            return Err(syn::Error::new_spanned(
98                equals,
99                "expected an expression following this equals sign",
100            ));
101        }
102
103        let value = parse_prop_value(input)?;
104        Ok(Self {
105            label,
106            value,
107            directive,
108        })
109    }
110}
111
112fn parse_prop_value(input: &ParseBuffer) -> syn::Result<Expr> {
113    if input.peek(Brace) {
114        strip_braces(input.parse()?)
115    } else {
116        let expr = if let Some(ExprRange {
117            start: Some(start), ..
118        }) = range_expression_peek(input)
119        {
120            // If a range expression is seen, treat the left-side expression as the value
121            // and leave the right-side expression to be parsed as a base expression
122            advance_until_next_dot2(input)?;
123            *start
124        } else {
125            input.parse()?
126        };
127
128        match &expr {
129            Expr::Lit(_) => Ok(expr),
130            ref exp => Err(syn::Error::new_spanned(
131                &expr,
132                format!(
133                    "the property value must be either a literal or enclosed in braces. Consider \
134                     adding braces around your expression.: {exp:#?}"
135                ),
136            )),
137        }
138    }
139}
140
141fn strip_braces(block: ExprBlock) -> syn::Result<Expr> {
142    match block {
143        ExprBlock {
144            block: Block { mut stmts, .. },
145            ..
146        } if stmts.len() == 1 => {
147            let stmt = stmts.remove(0);
148            match stmt {
149                Stmt::Expr(expr, None) => Ok(expr),
150                Stmt::Macro(mac) => Ok(Expr::Macro(ExprMacro {
151                    attrs: vec![],
152                    mac: mac.mac,
153                })),
154                // See issue #2267, we want to parse macro invocations as expressions
155                Stmt::Item(syn::Item::Macro(mac))
156                    if mac.ident.is_none() && mac.semi_token.is_none() =>
157                {
158                    Ok(Expr::Macro(syn::ExprMacro {
159                        attrs: mac.attrs,
160                        mac: mac.mac,
161                    }))
162                }
163                Stmt::Expr(_, Some(semi)) => Err(syn::Error::new_spanned(
164                    semi,
165                    "only an expression may be assigned as a property. Consider removing this \
166                     semicolon",
167                )),
168                _ => Err(syn::Error::new_spanned(
169                    stmt,
170                    "only an expression may be assigned as a property",
171                )),
172            }
173        }
174        block => Ok(Expr::Block(block)),
175    }
176}
177
178// Without advancing cursor, returns the range expression at the current cursor position if any
179fn range_expression_peek(input: &ParseBuffer) -> Option<ExprRange> {
180    match input.fork().parse::<Expr>().ok()? {
181        Expr::Range(range) => Some(range),
182        _ => None,
183    }
184}
185
186fn advance_until_next_dot2(input: &ParseBuffer) -> syn::Result<()> {
187    input.step(|cursor| {
188        let mut rest = *cursor;
189        let mut first_dot = None;
190        while let Some((tt, next)) = rest.token_tree() {
191            match &tt {
192                TokenTree::Punct(punct) if punct.as_char() == '.' => {
193                    if let Some(first_dot) = first_dot {
194                        return Ok(((), first_dot));
195                    } else {
196                        // Only consider dot as potential first if there is no spacing after it
197                        first_dot = if punct.spacing() == Spacing::Joint {
198                            Some(rest)
199                        } else {
200                            None
201                        };
202                    }
203                }
204                _ => {
205                    first_dot = None;
206                }
207            }
208            rest = next;
209        }
210        Err(cursor.error("no `..` found in expression"))
211    })
212}
213
214/// List of props sorted in alphabetical order*.
215///
216/// \*The "children" prop always comes last to match the behaviour of the `Properties` derive macro.
217///
218/// The list may contain multiple props with the same label.
219/// Use `check_no_duplicates` to ensure that there are no duplicates.
220pub struct PropList(Vec<Prop>);
221impl PropList {
222    /// Create a new `SortedPropList` from a vector of props.
223    /// The given `props` doesn't need to be sorted.
224    pub fn new(props: Vec<Prop>) -> Self {
225        Self(props)
226    }
227
228    fn position(&self, key: &str) -> Option<usize> {
229        self.0.iter().position(|it| it.label.to_string() == key)
230    }
231
232    /// Get the first prop with the given key.
233    pub fn get_by_label(&self, key: &str) -> Option<&Prop> {
234        self.0.iter().find(|it| it.label.to_string() == key)
235    }
236
237    /// Pop the first prop with the given key.
238    pub fn pop(&mut self, key: &str) -> Option<Prop> {
239        self.position(key).map(|i| self.0.remove(i))
240    }
241
242    /// Pop the prop with the given key and error if there are multiple ones.
243    pub fn pop_unique(&mut self, key: &str) -> syn::Result<Option<Prop>> {
244        let prop = self.pop(key);
245        if prop.is_some() {
246            if let Some(other_prop) = self.get_by_label(key) {
247                return Err(syn::Error::new_spanned(
248                    &other_prop.label,
249                    format!("`{key}` can only be specified once"),
250                ));
251            }
252        }
253
254        Ok(prop)
255    }
256
257    /// Turn the props into a vector of `Prop`.
258    pub fn into_vec(self) -> Vec<Prop> {
259        self.0
260    }
261
262    /// Iterate over all duplicate props in order of appearance.
263    fn iter_duplicates(&self) -> impl Iterator<Item = &Prop> {
264        self.0.windows(2).filter_map(|pair| {
265            let (a, b) = (&pair[0], &pair[1]);
266
267            if a.label == b.label {
268                Some(b)
269            } else {
270                None
271            }
272        })
273    }
274
275    /// Remove and return all props for which `filter` returns `true`.
276    pub fn drain_filter(&mut self, filter: impl FnMut(&Prop) -> bool) -> Self {
277        let (drained, others) = self.0.drain(..).partition(filter);
278        self.0 = others;
279        Self(drained)
280    }
281
282    /// Run the given function for all props and aggregate the errors.
283    /// If there's at least one error, the result will be `Result::Err`.
284    pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> {
285        crate::join_errors(self.0.iter().map(f).filter_map(Result::err))
286    }
287
288    /// Return an error for all duplicate props.
289    pub fn check_no_duplicates(&self) -> syn::Result<()> {
290        crate::join_errors(self.iter_duplicates().map(|prop| {
291            syn::Error::new_spanned(
292                &prop.label,
293                format!(
294                    "`{}` can only be specified once but is given here again",
295                    prop.label
296                ),
297            )
298        }))
299    }
300}
301impl Parse for PropList {
302    fn parse(input: ParseStream) -> syn::Result<Self> {
303        let mut props: Vec<Prop> = Vec::new();
304        // Stop parsing props if a base expression preceded by `..` is reached
305        while !input.is_empty() && !input.peek(Token![..]) {
306            props.push(input.parse()?);
307        }
308
309        Ok(Self::new(props))
310    }
311}
312impl Deref for PropList {
313    type Target = [Prop];
314
315    fn deref(&self) -> &Self::Target {
316        &self.0
317    }
318}
319
320#[derive(Default)]
321pub struct SpecialProps {
322    pub node_ref: Option<Prop>,
323    pub key: Option<Prop>,
324}
325impl SpecialProps {
326    const KEY_LABEL: &'static str = "key";
327    const REF_LABEL: &'static str = "ref";
328
329    fn pop_from(props: &mut PropList) -> syn::Result<Self> {
330        let node_ref = props.pop_unique(Self::REF_LABEL)?;
331        let key = props.pop_unique(Self::KEY_LABEL)?;
332        Ok(Self { node_ref, key })
333    }
334
335    fn iter(&self) -> impl Iterator<Item = &Prop> {
336        self.node_ref.as_ref().into_iter().chain(self.key.as_ref())
337    }
338
339    /// Run the given function for all props and aggregate the errors.
340    /// If there's at least one error, the result will be `Result::Err`.
341    pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> {
342        crate::join_errors(self.iter().map(f).filter_map(Result::err))
343    }
344
345    pub fn wrap_node_ref_attr(&self) -> TokenStream {
346        self.node_ref
347            .as_ref()
348            .map(|attr| {
349                let value = &attr.value;
350                quote_spanned! {value.span().resolved_at(Span::call_site())=>
351                    ::yew::html::IntoPropValue::<::yew::html::NodeRef>
352                    ::into_prop_value(#value)
353                }
354            })
355            .unwrap_or(quote! { ::std::default::Default::default() })
356    }
357
358    pub fn wrap_key_attr(&self) -> TokenStream {
359        self.key
360            .as_ref()
361            .map(|attr| {
362                let value = attr.value.optimize_literals();
363                quote_spanned! {value.span().resolved_at(Span::call_site())=>
364                    ::std::option::Option::Some(
365                        ::std::convert::Into::<::yew::virtual_dom::Key>::into(#value)
366                    )
367                }
368            })
369            .unwrap_or(quote! { ::std::option::Option::None })
370    }
371}
372
373pub struct Props {
374    pub special: SpecialProps,
375    pub prop_list: PropList,
376}
377impl Parse for Props {
378    fn parse(input: ParseStream) -> syn::Result<Self> {
379        Self::try_from(input.parse::<PropList>()?)
380    }
381}
382impl Deref for Props {
383    type Target = PropList;
384
385    fn deref(&self) -> &Self::Target {
386        &self.prop_list
387    }
388}
389impl DerefMut for Props {
390    fn deref_mut(&mut self) -> &mut Self::Target {
391        &mut self.prop_list
392    }
393}
394
395impl TryFrom<PropList> for Props {
396    type Error = syn::Error;
397
398    fn try_from(mut prop_list: PropList) -> Result<Self, Self::Error> {
399        let special = SpecialProps::pop_from(&mut prop_list)?;
400        Ok(Self { special, prop_list })
401    }
402}