dioxus_rsx/
attribute.rs

1//! Parser for the attribute shared both by elements and components
2//!
3//! ```rust, ignore
4//! rsx! {
5//!     div {
6//!         class: "my-class",
7//!         onclick: move |_| println!("clicked")
8//!     }
9//!
10//!     Component {
11//!         class: "my-class",
12//!         onclick: move |_| println!("clicked")
13//!     }
14//! }
15//! ```
16
17use super::literal::HotLiteral;
18use crate::{innerlude::*, partial_closure::PartialClosure};
19
20use proc_macro2::{Span, TokenStream as TokenStream2};
21use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
22use std::fmt::Display;
23use syn::{
24    ext::IdentExt,
25    parse::{Parse, ParseStream},
26    parse_quote,
27    spanned::Spanned,
28    Block, Expr, ExprBlock, ExprClosure, ExprIf, Ident, Lit, LitBool, LitFloat, LitInt, LitStr,
29    Stmt, Token,
30};
31
32/// A property value in the from of a `name: value` pair with an optional comma.
33/// Note that the colon and value are optional in the case of shorthand attributes. We keep them around
34/// to support "lossless" parsing in case that ever might be useful.
35#[derive(PartialEq, Eq, Clone, Debug, Hash)]
36pub struct Attribute {
37    /// The name of the attribute (ident or custom)
38    ///
39    /// IE `class` or `onclick`
40    pub name: AttributeName,
41
42    /// The colon that separates the name and value - keep this for lossless parsing
43    pub colon: Option<Token![:]>,
44
45    /// The value of the attribute
46    ///
47    /// IE `class="my-class"` or `onclick: move |_| println!("clicked")`
48    pub value: AttributeValue,
49
50    /// The comma that separates this attribute from the next one
51    /// Used for more accurate completions
52    pub comma: Option<Token![,]>,
53
54    /// The dynamic index of this attribute - used by the template system
55    pub dyn_idx: DynIdx,
56
57    /// The element name of this attribute if it is bound to an element.
58    /// When parsed for components or freestanding, this will be None
59    pub el_name: Option<ElementName>,
60}
61
62impl Parse for Attribute {
63    fn parse(content: ParseStream) -> syn::Result<Self> {
64        // if there's an ident not followed by a colon, it's a shorthand attribute
65        if content.peek(Ident::peek_any) && !content.peek2(Token![:]) {
66            let ident = parse_raw_ident(content)?;
67            let comma = content.parse().ok();
68
69            return Ok(Attribute {
70                name: AttributeName::BuiltIn(ident.clone()),
71                colon: None,
72                value: AttributeValue::Shorthand(ident),
73                comma,
74                dyn_idx: DynIdx::default(),
75                el_name: None,
76            });
77        }
78
79        // Parse the name as either a known or custom attribute
80        let name = match content.peek(LitStr) {
81            true => AttributeName::Custom(content.parse::<LitStr>()?),
82            false => AttributeName::BuiltIn(parse_raw_ident(content)?),
83        };
84
85        // Ensure there's a colon
86        let colon = Some(content.parse::<Token![:]>()?);
87
88        // todo: make this cleaner please
89        // if statements in attributes get automatic closing in some cases
90        // we shouldn't be handling it any differently.
91        let value = AttributeValue::parse(content)?;
92
93        let comma = content.parse::<Token![,]>().ok();
94
95        let attr = Attribute {
96            name,
97            value,
98            colon,
99            comma,
100            dyn_idx: DynIdx::default(),
101            el_name: None,
102        };
103
104        Ok(attr)
105    }
106}
107
108impl Attribute {
109    /// Create a new attribute from a name and value
110    pub fn from_raw(name: AttributeName, value: AttributeValue) -> Self {
111        Self {
112            name,
113            colon: Default::default(),
114            value,
115            comma: Default::default(),
116            dyn_idx: Default::default(),
117            el_name: None,
118        }
119    }
120
121    /// Set the dynamic index of this attribute
122    pub fn set_dyn_idx(&self, idx: usize) {
123        self.dyn_idx.set(idx);
124    }
125
126    /// Get the dynamic index of this attribute
127    pub fn get_dyn_idx(&self) -> usize {
128        self.dyn_idx.get()
129    }
130
131    pub fn span(&self) -> proc_macro2::Span {
132        self.name.span()
133    }
134
135    pub fn as_lit(&self) -> Option<&HotLiteral> {
136        match &self.value {
137            AttributeValue::AttrLiteral(lit) => Some(lit),
138            _ => None,
139        }
140    }
141
142    /// Run this closure against the attribute if it's hotreloadable
143    pub fn with_literal(&self, f: impl FnOnce(&HotLiteral)) {
144        if let AttributeValue::AttrLiteral(ifmt) = &self.value {
145            f(ifmt);
146        }
147    }
148
149    pub fn ifmt(&self) -> Option<&IfmtInput> {
150        match &self.value {
151            AttributeValue::AttrLiteral(HotLiteral::Fmted(input)) => Some(input),
152            _ => None,
153        }
154    }
155
156    pub fn as_static_str_literal(&self) -> Option<(&AttributeName, &IfmtInput)> {
157        match &self.value {
158            AttributeValue::AttrLiteral(lit) => match &lit {
159                HotLiteral::Fmted(input) if input.is_static() => Some((&self.name, input)),
160                _ => None,
161            },
162            _ => None,
163        }
164    }
165
166    pub fn is_static_str_literal(&self) -> bool {
167        self.as_static_str_literal().is_some()
168    }
169
170    pub fn rendered_as_dynamic_attr(&self) -> TokenStream2 {
171        // Shortcut out with spreads
172        if let AttributeName::Spread(_) = self.name {
173            let AttributeValue::AttrExpr(expr) = &self.value else {
174                unreachable!("Spread attributes should always be expressions")
175            };
176            return quote_spanned! { expr.span() => {#expr}.into_boxed_slice() };
177        }
178
179        let el_name = self
180            .el_name
181            .as_ref()
182            .expect("el_name rendered as a dynamic attribute should always have an el_name set");
183
184        let ns = |name: &AttributeName| match (el_name, name) {
185            (ElementName::Ident(i), AttributeName::BuiltIn(_)) => {
186                quote! { dioxus_elements::#i::#name.1 }
187            }
188            _ => quote! { None },
189        };
190
191        let volatile = |name: &AttributeName| match (el_name, name) {
192            (ElementName::Ident(i), AttributeName::BuiltIn(_)) => {
193                quote! { dioxus_elements::#i::#name.2 }
194            }
195            _ => quote! { false },
196        };
197
198        let attribute = |name: &AttributeName| match name {
199            AttributeName::BuiltIn(name) => match el_name {
200                ElementName::Ident(_) => quote! { dioxus_elements::#el_name::#name.0 },
201                ElementName::Custom(_) => {
202                    let as_string = name.to_string();
203                    quote!(#as_string)
204                }
205            },
206            AttributeName::Custom(s) => quote! { #s },
207            AttributeName::Spread(_) => unreachable!("Spread attributes are handled elsewhere"),
208        };
209
210        let attribute = {
211            let value = &self.value;
212            let name = &self.name;
213            let is_not_event = !self.name.is_likely_event();
214
215            match &self.value {
216                AttributeValue::AttrLiteral(_)
217                | AttributeValue::AttrExpr(_)
218                | AttributeValue::Shorthand(_)
219                | AttributeValue::IfExpr { .. }
220                    if is_not_event =>
221                {
222                    let name = &self.name;
223                    let ns = ns(name);
224                    let volatile = volatile(name);
225                    let attribute = attribute(name);
226                    let value = quote! { #value };
227
228                    quote! {
229                        dioxus_core::Attribute::new(
230                            #attribute,
231                            #value,
232                            #ns,
233                            #volatile
234                        )
235                    }
236                }
237                AttributeValue::EventTokens(_) | AttributeValue::AttrExpr(_) => {
238                    let (tokens, span) = match &self.value {
239                        AttributeValue::EventTokens(tokens) => {
240                            (tokens.to_token_stream(), tokens.span())
241                        }
242                        AttributeValue::AttrExpr(tokens) => {
243                            (tokens.to_token_stream(), tokens.span())
244                        }
245                        _ => unreachable!(),
246                    };
247
248                    fn check_tokens_is_closure(tokens: &TokenStream2) -> bool {
249                        if syn::parse2::<ExprClosure>(tokens.to_token_stream()).is_ok() {
250                            return true;
251                        }
252                        let Ok(block) = syn::parse2::<ExprBlock>(tokens.to_token_stream()) else {
253                            return false;
254                        };
255                        let mut block = &block;
256                        loop {
257                            match block.block.stmts.last() {
258                                Some(Stmt::Expr(Expr::Closure(_), _)) => return true,
259                                Some(Stmt::Expr(Expr::Block(b), _)) => {
260                                    block = b;
261                                    continue;
262                                }
263                                _ => return false,
264                            }
265                        }
266                    }
267                    match &self.name {
268                        AttributeName::BuiltIn(name) => {
269                            let event_tokens_is_closure = check_tokens_is_closure(&tokens);
270                            let function_name =
271                                quote_spanned! { span => dioxus_elements::events::#name };
272                            let function = if event_tokens_is_closure {
273                                // If we see an explicit closure, we can call the `call_with_explicit_closure` version of the event for better type inference
274                                quote_spanned! { span => #function_name::call_with_explicit_closure }
275                            } else {
276                                function_name
277                            };
278                            quote_spanned! { span =>
279                                #function(#tokens)
280                            }
281                        }
282                        AttributeName::Custom(_) => unreachable!("Handled elsewhere in the macro"),
283                        AttributeName::Spread(_) => unreachable!("Handled elsewhere in the macro"),
284                    }
285                }
286                _ => {
287                    quote_spanned! { value.span() => dioxus_elements::events::#name(#value) }
288                }
289            }
290        };
291
292        let attr_span = attribute.span();
293        let completion_hints = self.completion_hints();
294        quote_spanned! { attr_span =>
295            Box::new([
296                {
297                    #completion_hints
298                    #attribute
299                }
300            ])
301        }
302        .to_token_stream()
303    }
304
305    pub fn can_be_shorthand(&self) -> bool {
306        // If it's a shorthand...
307        if matches!(self.value, AttributeValue::Shorthand(_)) {
308            return true;
309        }
310
311        // Or if it is a builtin attribute with a single ident value
312        if let (AttributeName::BuiltIn(name), AttributeValue::AttrExpr(expr)) =
313            (&self.name, &self.value)
314        {
315            if let Ok(Expr::Path(path)) = expr.as_expr() {
316                if path.path.get_ident() == Some(name) {
317                    return true;
318                }
319            }
320        }
321
322        false
323    }
324
325    /// If this is the last attribute of an element and it doesn't have a tailing comma,
326    /// we add hints so that rust analyzer completes it either as an attribute or element
327    fn completion_hints(&self) -> TokenStream2 {
328        let Attribute {
329            name,
330            value,
331            comma,
332            el_name,
333            ..
334        } = self;
335
336        // If there is a trailing comma, rust analyzer does a good job of completing the attribute by itself
337        if comma.is_some() {
338            return quote! {};
339        }
340
341        // Only add hints if the attribute is:
342        // - a built in attribute (not a literal)
343        // - an build in element (not a custom element)
344        // - a shorthand attribute
345        let (
346            Some(ElementName::Ident(el)),
347            AttributeName::BuiltIn(name),
348            AttributeValue::Shorthand(_),
349        ) = (&el_name, &name, &value)
350        else {
351            return quote! {};
352        };
353        // If the attribute is a shorthand attribute, but it is an event handler, rust analyzer already does a good job of completing the attribute by itself
354        if name.to_string().starts_with("on") {
355            return quote! {};
356        }
357
358        quote! {
359            {
360                #[allow(dead_code)]
361                #[doc(hidden)]
362                mod __completions {
363                    // Autocomplete as an attribute
364                    pub use super::dioxus_elements::#el::*;
365                    // Autocomplete as an element
366                    pub use super::dioxus_elements::elements::completions::CompleteWithBraces::*;
367                    fn ignore() {
368                        #name;
369                    }
370                }
371            }
372        }
373    }
374}
375
376#[derive(PartialEq, Eq, Clone, Debug, Hash)]
377pub enum AttributeName {
378    Spread(Token![..]),
379
380    /// an attribute in the form of `name: value`
381    BuiltIn(Ident),
382
383    /// an attribute in the form of `"name": value` - notice that the name is a string literal
384    /// this is to allow custom attributes in the case of missing built-in attributes
385    ///
386    /// we might want to change this one day to be ticked or something and simply a boolean
387    Custom(LitStr),
388}
389
390impl AttributeName {
391    pub fn is_likely_event(&self) -> bool {
392        matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on"))
393    }
394
395    pub fn is_likely_key(&self) -> bool {
396        matches!(self, Self::BuiltIn(ident) if ident == "key")
397    }
398
399    pub fn span(&self) -> proc_macro2::Span {
400        match self {
401            Self::Custom(lit) => lit.span(),
402            Self::BuiltIn(ident) => ident.span(),
403            Self::Spread(dots) => dots.span(),
404        }
405    }
406}
407
408impl Display for AttributeName {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        match self {
411            Self::Custom(lit) => write!(f, "{}", lit.value()),
412            Self::BuiltIn(ident) => write!(f, "{}", ident),
413            Self::Spread(_) => write!(f, ".."),
414        }
415    }
416}
417
418impl ToTokens for AttributeName {
419    fn to_tokens(&self, tokens: &mut TokenStream2) {
420        match self {
421            Self::Custom(lit) => lit.to_tokens(tokens),
422            Self::BuiltIn(ident) => ident.to_tokens(tokens),
423            Self::Spread(dots) => dots.to_tokens(tokens),
424        }
425    }
426}
427
428// ..spread attribute
429#[derive(PartialEq, Eq, Clone, Debug, Hash)]
430pub struct Spread {
431    pub dots: Token![..],
432    pub expr: Expr,
433    pub dyn_idx: DynIdx,
434    pub comma: Option<Token![,]>,
435}
436
437impl Spread {
438    pub fn span(&self) -> proc_macro2::Span {
439        self.dots.span()
440    }
441}
442
443#[derive(PartialEq, Eq, Clone, Debug, Hash)]
444pub enum AttributeValue {
445    /// Just a regular shorthand attribute - an ident. Makes our parsing a bit more opaque.
446    /// attribute,
447    Shorthand(Ident),
448
449    /// Any attribute that's a literal. These get hotreloading super powers
450    ///
451    /// attribute: "value"
452    /// attribute: bool,
453    /// attribute: 1,
454    AttrLiteral(HotLiteral),
455
456    /// A series of tokens that represent an event handler
457    ///
458    /// We use a special type here so we can get autocomplete in the closure using partial expansion.
459    /// We also do some extra wrapping for improved type hinting since rust sometimes has trouble with
460    /// generics and closures.
461    EventTokens(PartialClosure),
462
463    /// Conditional expression
464    ///
465    /// attribute: if bool { "value" } else if bool { "other value" } else { "default value" }
466    ///
467    /// Currently these don't get hotreloading super powers, but they could, depending on how far
468    /// we want to go with it
469    IfExpr(IfAttributeValue),
470
471    /// attribute: some_expr
472    /// attribute: {some_expr} ?
473    AttrExpr(PartialExpr),
474}
475
476impl Parse for AttributeValue {
477    fn parse(content: ParseStream) -> syn::Result<Self> {
478        // Attempt to parse the unterminated if statement
479        if content.peek(Token![if]) {
480            return Ok(Self::IfExpr(content.parse::<IfAttributeValue>()?));
481        }
482
483        // Use the move and/or bars as an indicator that we have an event handler
484        if content.peek(Token![move]) || content.peek(Token![|]) {
485            let value = content.parse()?;
486            return Ok(AttributeValue::EventTokens(value));
487        }
488
489        if content.peek(LitStr)
490            || content.peek(LitBool)
491            || content.peek(LitFloat)
492            || content.peek(LitInt)
493        {
494            let fork = content.fork();
495            _ = fork.parse::<Lit>().unwrap();
496
497            if content.peek2(Token![,]) || fork.is_empty() {
498                let value = content.parse()?;
499                return Ok(AttributeValue::AttrLiteral(value));
500            }
501        }
502
503        let value = content.parse::<PartialExpr>()?;
504        Ok(AttributeValue::AttrExpr(value))
505    }
506}
507
508impl ToTokens for AttributeValue {
509    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
510        match self {
511            Self::Shorthand(ident) => ident.to_tokens(tokens),
512            Self::AttrLiteral(ifmt) => ifmt.to_tokens(tokens),
513            Self::IfExpr(if_expr) => if_expr.to_tokens(tokens),
514            Self::AttrExpr(expr) => expr.to_tokens(tokens),
515            Self::EventTokens(closure) => closure.to_tokens(tokens),
516        }
517    }
518}
519
520impl AttributeValue {
521    pub fn span(&self) -> proc_macro2::Span {
522        match self {
523            Self::Shorthand(ident) => ident.span(),
524            Self::AttrLiteral(ifmt) => ifmt.span(),
525            Self::IfExpr(if_expr) => if_expr.span(),
526            Self::AttrExpr(expr) => expr.span(),
527            Self::EventTokens(closure) => closure.span(),
528        }
529    }
530}
531
532/// A if else chain attribute value
533#[derive(PartialEq, Eq, Clone, Debug, Hash)]
534pub struct IfAttributeValue {
535    pub if_expr: ExprIf,
536    pub condition: Expr,
537    pub then_value: Box<AttributeValue>,
538    pub else_value: Option<Box<AttributeValue>>,
539}
540
541impl IfAttributeValue {
542    /// Convert the if expression to an expression that returns a string. If the unterminated case is hit, it returns an empty string
543    pub(crate) fn quote_as_string(&self, diagnostics: &mut Diagnostics) -> Expr {
544        let mut expression = quote! {};
545        let mut current_if_value = self;
546
547        let mut non_string_diagnostic = |span: proc_macro2::Span| -> Expr {
548            Element::add_merging_non_string_diagnostic(diagnostics, span);
549            parse_quote! { ::std::string::String::new() }
550        };
551
552        loop {
553            let AttributeValue::AttrLiteral(lit) = current_if_value.then_value.as_ref() else {
554                return non_string_diagnostic(current_if_value.span());
555            };
556
557            let HotLiteral::Fmted(HotReloadFormattedSegment {
558                formatted_input: new,
559                ..
560            }) = &lit
561            else {
562                return non_string_diagnostic(current_if_value.span());
563            };
564
565            let condition = &current_if_value.if_expr.cond;
566            expression.extend(quote! {
567                if #condition {
568                    #new.to_string()
569                } else
570            });
571            match current_if_value.else_value.as_deref() {
572                // If the else value is another if expression, then we need to continue the loop
573                Some(AttributeValue::IfExpr(else_value)) => {
574                    current_if_value = else_value;
575                }
576                // If the else value is a literal, then we need to append it to the expression and break
577                Some(AttributeValue::AttrLiteral(lit)) => {
578                    if let HotLiteral::Fmted(new) = &lit {
579                        let fmted = &new.formatted_input;
580                        expression.extend(quote! { { #fmted.to_string() } });
581                        break;
582                    } else {
583                        return non_string_diagnostic(current_if_value.span());
584                    }
585                }
586                // If it is the end of the if expression without an else, then we need to append the default value and break
587                None => {
588                    expression.extend(quote! { { ::std::string::String::new() } });
589                    break;
590                }
591                _ => {
592                    return non_string_diagnostic(current_if_value.else_value.span());
593                }
594            }
595        }
596
597        parse_quote! {
598            {
599                #expression
600            }
601        }
602    }
603
604    fn span(&self) -> Span {
605        self.if_expr.span()
606    }
607
608    fn is_terminated(&self) -> bool {
609        match &self.else_value {
610            Some(attribute) => match attribute.as_ref() {
611                AttributeValue::IfExpr(if_expr) => if_expr.is_terminated(),
612                _ => true,
613            },
614            None => false,
615        }
616    }
617
618    fn contains_expression(&self) -> bool {
619        fn attribute_value_contains_expression(expr: &AttributeValue) -> bool {
620            match expr {
621                AttributeValue::IfExpr(if_expr) => if_expr.contains_expression(),
622                AttributeValue::AttrLiteral(_) => false,
623                _ => true,
624            }
625        }
626
627        attribute_value_contains_expression(&self.then_value)
628            || self
629                .else_value
630                .as_deref()
631                .is_some_and(attribute_value_contains_expression)
632    }
633
634    fn parse_attribute_value_from_block(block: &Block) -> syn::Result<Box<AttributeValue>> {
635        let stmts = &block.stmts;
636
637        if stmts.len() != 1 {
638            return Err(syn::Error::new(
639                block.span(),
640                "Expected a single statement in the if block",
641            ));
642        }
643
644        // either an ifmt or an expr in the block
645        let stmt = &stmts[0];
646
647        // Either it's a valid ifmt or an expression
648        match stmt {
649            syn::Stmt::Expr(exp, None) => {
650                // Try parsing the statement as an IfmtInput by passing it through tokens
651                let value: Result<HotLiteral, syn::Error> = syn::parse2(exp.to_token_stream());
652                Ok(match value {
653                    Ok(res) => Box::new(AttributeValue::AttrLiteral(res)),
654                    Err(_) => Box::new(AttributeValue::AttrExpr(PartialExpr::from_expr(exp))),
655                })
656            }
657            _ => Err(syn::Error::new(stmt.span(), "Expected an expression")),
658        }
659    }
660
661    fn to_tokens_with_terminated(
662        &self,
663        tokens: &mut TokenStream2,
664        terminated: bool,
665        contains_expression: bool,
666    ) {
667        let IfAttributeValue {
668            if_expr,
669            then_value,
670            else_value,
671            ..
672        } = self;
673
674        // Quote an attribute value and convert the value to a string if it is formatted
675        // We always quote formatted segments as strings inside if statements so they have a consistent type
676        // This fixes https://github.com/DioxusLabs/dioxus/issues/2997
677        fn quote_attribute_value_string(
678            value: &AttributeValue,
679            contains_expression: bool,
680        ) -> TokenStream2 {
681            if let AttributeValue::AttrLiteral(HotLiteral::Fmted(fmted)) = value {
682                if let Some(str) = fmted.to_static().filter(|_| contains_expression) {
683                    // If this is actually a static string, the user may be using a static string expression in another branch
684                    // use into to convert the string to whatever the other branch is using
685                    quote! {
686                        {
687                            #[allow(clippy::useless_conversion)]
688                            #str.into()
689                        }
690                    }
691                } else {
692                    quote! { #value.to_string() }
693                }
694            } else {
695                value.to_token_stream()
696            }
697        }
698
699        let then_value = quote_attribute_value_string(then_value, contains_expression);
700
701        let then_value = if terminated {
702            quote! { #then_value }
703        }
704        // Otherwise we need to return an Option and a None if the else value is None
705        else {
706            quote! { Some(#then_value) }
707        };
708
709        let else_value = match else_value.as_deref() {
710            Some(AttributeValue::IfExpr(else_value)) => {
711                let mut tokens = TokenStream2::new();
712                else_value.to_tokens_with_terminated(&mut tokens, terminated, contains_expression);
713                tokens
714            }
715            Some(other) => {
716                let other = quote_attribute_value_string(other, contains_expression);
717                if terminated {
718                    other
719                } else {
720                    quote_spanned! { other.span() => Some(#other) }
721                }
722            }
723            None => quote! { None },
724        };
725
726        let condition = &if_expr.cond;
727        tokens.append_all(quote_spanned! { if_expr.span()=>
728            if #condition {
729                #then_value
730            } else {
731                #else_value
732            }
733        });
734    }
735}
736
737impl Parse for IfAttributeValue {
738    fn parse(input: ParseStream) -> syn::Result<Self> {
739        let if_expr = input.parse::<ExprIf>()?;
740
741        let stmts = &if_expr.then_branch.stmts;
742
743        if stmts.len() != 1 {
744            return Err(syn::Error::new(
745                if_expr.then_branch.span(),
746                "Expected a single statement in the if block",
747            ));
748        }
749
750        // Parse the then branch into a single attribute value
751        let then_value = Self::parse_attribute_value_from_block(&if_expr.then_branch)?;
752
753        // If there's an else branch, parse it as a single attribute value or an if expression
754        let else_value = match if_expr.else_branch.as_ref() {
755            Some((_, else_branch)) => {
756                // The else branch if either a block or another if expression
757                let attribute_value = match else_branch.as_ref() {
758                    // If it is a block, then the else is terminated
759                    Expr::Block(block) => Self::parse_attribute_value_from_block(&block.block)?,
760                    // Otherwise try to parse it as an if expression
761                    _ => Box::new(syn::parse2(else_branch.to_token_stream())?),
762                };
763                Some(attribute_value)
764            }
765            None => None,
766        };
767
768        Ok(Self {
769            condition: *if_expr.cond.clone(),
770            if_expr,
771            then_value,
772            else_value,
773        })
774    }
775}
776
777impl ToTokens for IfAttributeValue {
778    fn to_tokens(&self, tokens: &mut TokenStream2) {
779        // If the if expression is terminated, we can just return the then value
780        let terminated = self.is_terminated();
781        let contains_expression = self.contains_expression();
782        self.to_tokens_with_terminated(tokens, terminated, contains_expression)
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789    use quote::quote;
790    use syn::parse2;
791
792    #[test]
793    fn parse_attrs() {
794        let _parsed: Attribute = parse2(quote! { name: "value" }).unwrap();
795        let _parsed: Attribute = parse2(quote! { name: value }).unwrap();
796        let _parsed: Attribute = parse2(quote! { name: "value {fmt}" }).unwrap();
797        let _parsed: Attribute = parse2(quote! { name: 123 }).unwrap();
798        let _parsed: Attribute = parse2(quote! { name: false }).unwrap();
799        let _parsed: Attribute = parse2(quote! { "custom": false }).unwrap();
800        let _parsed: Attribute = parse2(quote! { prop: "blah".to_string() }).unwrap();
801
802        // with commas
803        let _parsed: Attribute = parse2(quote! { "custom": false, }).unwrap();
804        let _parsed: Attribute = parse2(quote! { name: false, }).unwrap();
805
806        // with if chains
807        let parsed: Attribute = parse2(quote! { name: if true { "value" } }).unwrap();
808        assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
809        let parsed: Attribute =
810            parse2(quote! { name: if true { "value" } else { "other" } }).unwrap();
811        assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
812        let parsed: Attribute =
813            parse2(quote! { name: if true { "value" } else if false { "other" } }).unwrap();
814        assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
815
816        // with shorthand
817        let _parsed: Attribute = parse2(quote! { name }).unwrap();
818        let _parsed: Attribute = parse2(quote! { name, }).unwrap();
819
820        // Events - make sure they get partial expansion
821        let parsed: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
822        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
823        let parsed: Attribute = parse2(quote! { onclick: |e| { "value" } }).unwrap();
824        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
825        let parsed: Attribute = parse2(quote! { onclick: |e| { value. } }).unwrap();
826        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
827        let parsed: Attribute = parse2(quote! { onclick: move |e| { value. } }).unwrap();
828        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
829        let parsed: Attribute = parse2(quote! { onclick: move |e| value }).unwrap();
830        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
831        let parsed: Attribute = parse2(quote! { onclick: |e| value, }).unwrap();
832        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
833    }
834
835    #[test]
836    fn merge_attrs() {
837        let _a: Attribute = parse2(quote! { class: "value1" }).unwrap();
838        let _b: Attribute = parse2(quote! { class: "value2" }).unwrap();
839
840        let _b: Attribute = parse2(quote! { class: "value2 {something}" }).unwrap();
841        let _b: Attribute = parse2(quote! { class: if value { "other thing" } }).unwrap();
842        let _b: Attribute = parse2(quote! { class: if value { some_expr } }).unwrap();
843
844        let _b: Attribute = parse2(quote! { class: if value { "some_expr" } }).unwrap();
845        dbg!(_b);
846    }
847
848    #[test]
849    fn static_literals() {
850        let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
851        let b: Attribute = parse2(quote! { class: "value {some}" }).unwrap();
852
853        assert!(a.is_static_str_literal());
854        assert!(!b.is_static_str_literal());
855    }
856
857    #[test]
858    fn partial_eqs() {
859        // Basics
860        let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
861        let b: Attribute = parse2(quote! { class: "value1" }).unwrap();
862        assert_eq!(a, b);
863
864        // Exprs
865        let a: Attribute = parse2(quote! { class: var }).unwrap();
866        let b: Attribute = parse2(quote! { class: var }).unwrap();
867        assert_eq!(a, b);
868
869        // Events
870        let a: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
871        let b: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
872        let c: Attribute = parse2(quote! { onclick: move |e| {} }).unwrap();
873        let d: Attribute = parse2(quote! { onclick: { |e| {} } }).unwrap();
874        assert_eq!(a, b);
875        assert_ne!(a, c);
876        assert_ne!(a, d);
877    }
878
879    #[test]
880    fn call_with_explicit_closure() {
881        let mut a: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
882        a.el_name = Some(parse_quote!(button));
883        assert!(a
884            .rendered_as_dynamic_attr()
885            .to_string()
886            .contains("call_with_explicit_closure"));
887
888        let mut a: Attribute = parse2(quote! { onclick: { let a = 1; |e| {} } }).unwrap();
889        a.el_name = Some(parse_quote!(button));
890        assert!(a
891            .rendered_as_dynamic_attr()
892            .to_string()
893            .contains("call_with_explicit_closure"));
894
895        let mut a: Attribute = parse2(quote! { onclick: { let b = 2; { |e| { b } } } }).unwrap();
896        a.el_name = Some(parse_quote!(button));
897        assert!(a
898            .rendered_as_dynamic_attr()
899            .to_string()
900            .contains("call_with_explicit_closure"));
901
902        let mut a: Attribute = parse2(quote! { onclick: { let r = |e| { b }; r } }).unwrap();
903        a.el_name = Some(parse_quote!(button));
904        assert!(!a
905            .rendered_as_dynamic_attr()
906            .to_string()
907            .contains("call_with_explicit_closure"));
908    }
909
910    /// Make sure reserved keywords are parsed as attributes
911    /// HTML gets annoying sometimes so we just accept them
912    #[test]
913    fn reserved_keywords() {
914        let _a: Attribute = parse2(quote! { for: "class" }).unwrap();
915        let _b: Attribute = parse2(quote! { type: "class" }).unwrap();
916    }
917}