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::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! { {#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 completion_hints = self.completion_hints();
293        quote! {
294            Box::new([
295                {
296                    #completion_hints
297                    #attribute
298                }
299            ])
300        }
301        .to_token_stream()
302    }
303
304    pub fn can_be_shorthand(&self) -> bool {
305        // If it's a shorthand...
306        if matches!(self.value, AttributeValue::Shorthand(_)) {
307            return true;
308        }
309
310        // Or if it is a builtin attribute with a single ident value
311        if let (AttributeName::BuiltIn(name), AttributeValue::AttrExpr(expr)) =
312            (&self.name, &self.value)
313        {
314            if let Ok(Expr::Path(path)) = expr.as_expr() {
315                if path.path.get_ident() == Some(name) {
316                    return true;
317                }
318            }
319        }
320
321        false
322    }
323
324    /// If this is the last attribute of an element and it doesn't have a tailing comma,
325    /// we add hints so that rust analyzer completes it either as an attribute or element
326    fn completion_hints(&self) -> TokenStream2 {
327        let Attribute {
328            name,
329            value,
330            comma,
331            el_name,
332            ..
333        } = self;
334
335        // If there is a trailing comma, rust analyzer does a good job of completing the attribute by itself
336        if comma.is_some() {
337            return quote! {};
338        }
339
340        // Only add hints if the attribute is:
341        // - a built in attribute (not a literal)
342        // - an build in element (not a custom element)
343        // - a shorthand attribute
344        let (
345            Some(ElementName::Ident(el)),
346            AttributeName::BuiltIn(name),
347            AttributeValue::Shorthand(_),
348        ) = (&el_name, &name, &value)
349        else {
350            return quote! {};
351        };
352        // 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
353        if name.to_string().starts_with("on") {
354            return quote! {};
355        }
356
357        quote! {
358            {
359                #[allow(dead_code)]
360                #[doc(hidden)]
361                mod __completions {
362                    // Autocomplete as an attribute
363                    pub use super::dioxus_elements::#el::*;
364                    // Autocomplete as an element
365                    pub use super::dioxus_elements::elements::completions::CompleteWithBraces::*;
366                    fn ignore() {
367                        #name;
368                    }
369                }
370            }
371        }
372    }
373}
374
375#[derive(PartialEq, Eq, Clone, Debug, Hash)]
376pub enum AttributeName {
377    Spread(Token![..]),
378
379    /// an attribute in the form of `name: value`
380    BuiltIn(Ident),
381
382    /// an attribute in the form of `"name": value` - notice that the name is a string literal
383    /// this is to allow custom attributes in the case of missing built-in attributes
384    ///
385    /// we might want to change this one day to be ticked or something and simply a boolean
386    Custom(LitStr),
387}
388
389impl AttributeName {
390    pub fn is_likely_event(&self) -> bool {
391        matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on"))
392    }
393
394    pub fn is_likely_key(&self) -> bool {
395        matches!(self, Self::BuiltIn(ident) if ident == "key")
396    }
397
398    pub fn span(&self) -> proc_macro2::Span {
399        match self {
400            Self::Custom(lit) => lit.span(),
401            Self::BuiltIn(ident) => ident.span(),
402            Self::Spread(dots) => dots.span(),
403        }
404    }
405}
406
407impl Display for AttributeName {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        match self {
410            Self::Custom(lit) => write!(f, "{}", lit.value()),
411            Self::BuiltIn(ident) => write!(f, "{}", ident),
412            Self::Spread(_) => write!(f, ".."),
413        }
414    }
415}
416
417impl ToTokens for AttributeName {
418    fn to_tokens(&self, tokens: &mut TokenStream2) {
419        match self {
420            Self::Custom(lit) => lit.to_tokens(tokens),
421            Self::BuiltIn(ident) => ident.to_tokens(tokens),
422            Self::Spread(dots) => dots.to_tokens(tokens),
423        }
424    }
425}
426
427// ..spread attribute
428#[derive(PartialEq, Eq, Clone, Debug, Hash)]
429pub struct Spread {
430    pub dots: Token![..],
431    pub expr: Expr,
432    pub dyn_idx: DynIdx,
433    pub comma: Option<Token![,]>,
434}
435
436impl Spread {
437    pub fn span(&self) -> proc_macro2::Span {
438        self.dots.span()
439    }
440}
441
442#[derive(PartialEq, Eq, Clone, Debug, Hash)]
443pub enum AttributeValue {
444    /// Just a regular shorthand attribute - an ident. Makes our parsing a bit more opaque.
445    /// attribute,
446    Shorthand(Ident),
447
448    /// Any attribute that's a literal. These get hotreloading super powers
449    ///
450    /// attribute: "value"
451    /// attribute: bool,
452    /// attribute: 1,
453    AttrLiteral(HotLiteral),
454
455    /// A series of tokens that represent an event handler
456    ///
457    /// We use a special type here so we can get autocomplete in the closure using partial expansion.
458    /// We also do some extra wrapping for improved type hinting since rust sometimes has trouble with
459    /// generics and closures.
460    EventTokens(PartialClosure),
461
462    /// Conditional expression
463    ///
464    /// attribute: if bool { "value" } else if bool { "other value" } else { "default value" }
465    ///
466    /// Currently these don't get hotreloading super powers, but they could, depending on how far
467    /// we want to go with it
468    IfExpr(IfAttributeValue),
469
470    /// attribute: some_expr
471    /// attribute: {some_expr} ?
472    AttrExpr(PartialExpr),
473}
474
475impl Parse for AttributeValue {
476    fn parse(content: ParseStream) -> syn::Result<Self> {
477        // Attempt to parse the unterminated if statement
478        if content.peek(Token![if]) {
479            return Ok(Self::IfExpr(content.parse::<IfAttributeValue>()?));
480        }
481
482        // Use the move and/or bars as an indicator that we have an event handler
483        if content.peek(Token![move]) || content.peek(Token![|]) {
484            let value = content.parse()?;
485            return Ok(AttributeValue::EventTokens(value));
486        }
487
488        if content.peek(LitStr)
489            || content.peek(LitBool)
490            || content.peek(LitFloat)
491            || content.peek(LitInt)
492        {
493            let fork = content.fork();
494            _ = fork.parse::<Lit>().unwrap();
495
496            if content.peek2(Token![,]) || fork.is_empty() {
497                let value = content.parse()?;
498                return Ok(AttributeValue::AttrLiteral(value));
499            }
500        }
501
502        let value = content.parse::<PartialExpr>()?;
503        Ok(AttributeValue::AttrExpr(value))
504    }
505}
506
507impl ToTokens for AttributeValue {
508    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
509        match self {
510            Self::Shorthand(ident) => ident.to_tokens(tokens),
511            Self::AttrLiteral(ifmt) => ifmt.to_tokens(tokens),
512            Self::IfExpr(if_expr) => if_expr.to_tokens(tokens),
513            Self::AttrExpr(expr) => expr.to_tokens(tokens),
514            Self::EventTokens(closure) => closure.to_tokens(tokens),
515        }
516    }
517}
518
519impl AttributeValue {
520    pub fn span(&self) -> proc_macro2::Span {
521        match self {
522            Self::Shorthand(ident) => ident.span(),
523            Self::AttrLiteral(ifmt) => ifmt.span(),
524            Self::IfExpr(if_expr) => if_expr.span(),
525            Self::AttrExpr(expr) => expr.span(),
526            Self::EventTokens(closure) => closure.span(),
527        }
528    }
529}
530
531/// A if else chain attribute value
532#[derive(PartialEq, Eq, Clone, Debug, Hash)]
533pub struct IfAttributeValue {
534    pub condition: Expr,
535    pub then_value: Box<AttributeValue>,
536    pub else_value: Option<Box<AttributeValue>>,
537}
538
539impl IfAttributeValue {
540    /// Convert the if expression to an expression that returns a string. If the unterminated case is hit, it returns an empty string
541    pub(crate) fn quote_as_string(&self, diagnostics: &mut Diagnostics) -> Expr {
542        let mut expression = quote! {};
543        let mut current_if_value = self;
544
545        let mut non_string_diagnostic = |span: proc_macro2::Span| -> Expr {
546            Element::add_merging_non_string_diagnostic(diagnostics, span);
547            parse_quote! { ::std::string::String::new() }
548        };
549
550        loop {
551            let AttributeValue::AttrLiteral(lit) = current_if_value.then_value.as_ref() else {
552                return non_string_diagnostic(current_if_value.span());
553            };
554
555            let HotLiteral::Fmted(HotReloadFormattedSegment {
556                formatted_input: new,
557                ..
558            }) = &lit
559            else {
560                return non_string_diagnostic(current_if_value.span());
561            };
562
563            let condition = &current_if_value.condition;
564            expression.extend(quote! {
565                if #condition {
566                    #new.to_string()
567                } else
568            });
569            match current_if_value.else_value.as_deref() {
570                // If the else value is another if expression, then we need to continue the loop
571                Some(AttributeValue::IfExpr(else_value)) => {
572                    current_if_value = else_value;
573                }
574                // If the else value is a literal, then we need to append it to the expression and break
575                Some(AttributeValue::AttrLiteral(lit)) => {
576                    if let HotLiteral::Fmted(new) = &lit {
577                        let fmted = &new.formatted_input;
578                        expression.extend(quote! { { #fmted.to_string() } });
579                        break;
580                    } else {
581                        return non_string_diagnostic(current_if_value.span());
582                    }
583                }
584                // If it is the end of the if expression without an else, then we need to append the default value and break
585                None => {
586                    expression.extend(quote! { { ::std::string::String::new() } });
587                    break;
588                }
589                _ => {
590                    return non_string_diagnostic(current_if_value.else_value.span());
591                }
592            }
593        }
594
595        parse_quote! {
596            {
597                #expression
598            }
599        }
600    }
601
602    fn span(&self) -> proc_macro2::Span {
603        self.then_value.span()
604    }
605
606    fn is_terminated(&self) -> bool {
607        match &self.else_value {
608            Some(attribute) => match attribute.as_ref() {
609                AttributeValue::IfExpr(if_expr) => if_expr.is_terminated(),
610                _ => true,
611            },
612            None => false,
613        }
614    }
615
616    fn contains_expression(&self) -> bool {
617        fn attribute_value_contains_expression(expr: &AttributeValue) -> bool {
618            match expr {
619                AttributeValue::IfExpr(if_expr) => if_expr.contains_expression(),
620                AttributeValue::AttrLiteral(_) => false,
621                _ => true,
622            }
623        }
624
625        attribute_value_contains_expression(&self.then_value)
626            || self
627                .else_value
628                .as_deref()
629                .is_some_and(attribute_value_contains_expression)
630    }
631
632    fn parse_attribute_value_from_block(block: &Block) -> syn::Result<Box<AttributeValue>> {
633        let stmts = &block.stmts;
634
635        if stmts.len() != 1 {
636            return Err(syn::Error::new(
637                block.span(),
638                "Expected a single statement in the if block",
639            ));
640        }
641
642        // either an ifmt or an expr in the block
643        let stmt = &stmts[0];
644
645        // Either it's a valid ifmt or an expression
646        match stmt {
647            syn::Stmt::Expr(exp, None) => {
648                // Try parsing the statement as an IfmtInput by passing it through tokens
649                let value: Result<HotLiteral, syn::Error> = syn::parse2(quote! { #exp });
650                Ok(match value {
651                    Ok(res) => Box::new(AttributeValue::AttrLiteral(res)),
652                    Err(_) => Box::new(AttributeValue::AttrExpr(PartialExpr::from_expr(exp))),
653                })
654            }
655            _ => Err(syn::Error::new(stmt.span(), "Expected an expression")),
656        }
657    }
658
659    fn to_tokens_with_terminated(
660        &self,
661        tokens: &mut TokenStream2,
662        terminated: bool,
663        contains_expression: bool,
664    ) {
665        let IfAttributeValue {
666            condition,
667            then_value,
668            else_value,
669        } = self;
670
671        // Quote an attribute value and convert the value to a string if it is formatted
672        // We always quote formatted segments as strings inside if statements so they have a consistent type
673        // This fixes https://github.com/DioxusLabs/dioxus/issues/2997
674        fn quote_attribute_value_string(
675            value: &AttributeValue,
676            contains_expression: bool,
677        ) -> TokenStream2 {
678            if let AttributeValue::AttrLiteral(HotLiteral::Fmted(fmted)) = value {
679                if let Some(str) = fmted.to_static().filter(|_| contains_expression) {
680                    // If this is actually a static string, the user may be using a static string expression in another branch
681                    // use into to convert the string to whatever the other branch is using
682                    quote! {
683                        {
684                            #[allow(clippy::useless_conversion)]
685                            #str.into()
686                        }
687                    }
688                } else {
689                    quote! { #value.to_string() }
690                }
691            } else {
692                value.to_token_stream()
693            }
694        }
695
696        let then_value = quote_attribute_value_string(then_value, contains_expression);
697
698        let then_value = if terminated {
699            quote! { #then_value }
700        }
701        // Otherwise we need to return an Option and a None if the else value is None
702        else {
703            quote! { Some(#then_value) }
704        };
705
706        let else_value = match else_value.as_deref() {
707            Some(AttributeValue::IfExpr(else_value)) => {
708                let mut tokens = TokenStream2::new();
709                else_value.to_tokens_with_terminated(&mut tokens, terminated, contains_expression);
710                tokens
711            }
712            Some(other) => {
713                let other = quote_attribute_value_string(other, contains_expression);
714                if terminated {
715                    quote! { #other }
716                } else {
717                    quote! { Some(#other) }
718                }
719            }
720            None => quote! { None },
721        };
722
723        tokens.append_all(quote! {
724            {
725                if #condition {
726                    #then_value
727                } else {
728                    #else_value
729                }
730            }
731        });
732    }
733}
734
735impl Parse for IfAttributeValue {
736    fn parse(input: ParseStream) -> syn::Result<Self> {
737        let if_expr = input.parse::<ExprIf>()?;
738
739        let stmts = &if_expr.then_branch.stmts;
740
741        if stmts.len() != 1 {
742            return Err(syn::Error::new(
743                if_expr.then_branch.span(),
744                "Expected a single statement in the if block",
745            ));
746        }
747
748        // Parse the then branch into a single attribute value
749        let then_value = Self::parse_attribute_value_from_block(&if_expr.then_branch)?;
750
751        // If there's an else branch, parse it as a single attribute value or an if expression
752        let else_value = match if_expr.else_branch.as_ref() {
753            Some((_, else_branch)) => {
754                // The else branch if either a block or another if expression
755                let attribute_value = match else_branch.as_ref() {
756                    // If it is a block, then the else is terminated
757                    Expr::Block(block) => Self::parse_attribute_value_from_block(&block.block)?,
758                    // Otherwise try to parse it as an if expression
759                    _ => Box::new(syn::parse2(quote! { #else_branch })?),
760                };
761                Some(attribute_value)
762            }
763            None => None,
764        };
765
766        Ok(Self {
767            condition: *if_expr.cond,
768            then_value,
769            else_value,
770        })
771    }
772}
773
774impl ToTokens for IfAttributeValue {
775    fn to_tokens(&self, tokens: &mut TokenStream2) {
776        // If the if expression is terminated, we can just return the then value
777        let terminated = self.is_terminated();
778        let contains_expression = self.contains_expression();
779        self.to_tokens_with_terminated(tokens, terminated, contains_expression)
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use quote::quote;
787    use syn::parse2;
788
789    #[test]
790    fn parse_attrs() {
791        let _parsed: Attribute = parse2(quote! { name: "value" }).unwrap();
792        let _parsed: Attribute = parse2(quote! { name: value }).unwrap();
793        let _parsed: Attribute = parse2(quote! { name: "value {fmt}" }).unwrap();
794        let _parsed: Attribute = parse2(quote! { name: 123 }).unwrap();
795        let _parsed: Attribute = parse2(quote! { name: false }).unwrap();
796        let _parsed: Attribute = parse2(quote! { "custom": false }).unwrap();
797        let _parsed: Attribute = parse2(quote! { prop: "blah".to_string() }).unwrap();
798
799        // with commas
800        let _parsed: Attribute = parse2(quote! { "custom": false, }).unwrap();
801        let _parsed: Attribute = parse2(quote! { name: false, }).unwrap();
802
803        // with if chains
804        let parsed: Attribute = parse2(quote! { name: if true { "value" } }).unwrap();
805        assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
806        let parsed: Attribute =
807            parse2(quote! { name: if true { "value" } else { "other" } }).unwrap();
808        assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
809        let parsed: Attribute =
810            parse2(quote! { name: if true { "value" } else if false { "other" } }).unwrap();
811        assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
812
813        // with shorthand
814        let _parsed: Attribute = parse2(quote! { name }).unwrap();
815        let _parsed: Attribute = parse2(quote! { name, }).unwrap();
816
817        // Events - make sure they get partial expansion
818        let parsed: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
819        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
820        let parsed: Attribute = parse2(quote! { onclick: |e| { "value" } }).unwrap();
821        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
822        let parsed: Attribute = parse2(quote! { onclick: |e| { value. } }).unwrap();
823        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
824        let parsed: Attribute = parse2(quote! { onclick: move |e| { value. } }).unwrap();
825        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
826        let parsed: Attribute = parse2(quote! { onclick: move |e| value }).unwrap();
827        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
828        let parsed: Attribute = parse2(quote! { onclick: |e| value, }).unwrap();
829        assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
830    }
831
832    #[test]
833    fn merge_attrs() {
834        let _a: Attribute = parse2(quote! { class: "value1" }).unwrap();
835        let _b: Attribute = parse2(quote! { class: "value2" }).unwrap();
836
837        let _b: Attribute = parse2(quote! { class: "value2 {something}" }).unwrap();
838        let _b: Attribute = parse2(quote! { class: if value { "other thing" } }).unwrap();
839        let _b: Attribute = parse2(quote! { class: if value { some_expr } }).unwrap();
840
841        let _b: Attribute = parse2(quote! { class: if value { "some_expr" } }).unwrap();
842        dbg!(_b);
843    }
844
845    #[test]
846    fn static_literals() {
847        let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
848        let b: Attribute = parse2(quote! { class: "value {some}" }).unwrap();
849
850        assert!(a.is_static_str_literal());
851        assert!(!b.is_static_str_literal());
852    }
853
854    #[test]
855    fn partial_eqs() {
856        // Basics
857        let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
858        let b: Attribute = parse2(quote! { class: "value1" }).unwrap();
859        assert_eq!(a, b);
860
861        // Exprs
862        let a: Attribute = parse2(quote! { class: var }).unwrap();
863        let b: Attribute = parse2(quote! { class: var }).unwrap();
864        assert_eq!(a, b);
865
866        // Events
867        let a: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
868        let b: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
869        let c: Attribute = parse2(quote! { onclick: move |e| {} }).unwrap();
870        let d: Attribute = parse2(quote! { onclick: { |e| {} } }).unwrap();
871        assert_eq!(a, b);
872        assert_ne!(a, c);
873        assert_ne!(a, d);
874    }
875
876    #[test]
877    fn call_with_explicit_closure() {
878        let mut a: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
879        a.el_name = Some(parse_quote!(button));
880        assert!(a
881            .rendered_as_dynamic_attr()
882            .to_string()
883            .contains("call_with_explicit_closure"));
884
885        let mut a: Attribute = parse2(quote! { onclick: { let a = 1; |e| {} } }).unwrap();
886        a.el_name = Some(parse_quote!(button));
887        assert!(a
888            .rendered_as_dynamic_attr()
889            .to_string()
890            .contains("call_with_explicit_closure"));
891
892        let mut a: Attribute = parse2(quote! { onclick: { let b = 2; { |e| { b } } } }).unwrap();
893        a.el_name = Some(parse_quote!(button));
894        assert!(a
895            .rendered_as_dynamic_attr()
896            .to_string()
897            .contains("call_with_explicit_closure"));
898
899        let mut a: Attribute = parse2(quote! { onclick: { let r = |e| { b }; r } }).unwrap();
900        a.el_name = Some(parse_quote!(button));
901        assert!(!a
902            .rendered_as_dynamic_attr()
903            .to_string()
904            .contains("call_with_explicit_closure"));
905    }
906
907    /// Make sure reserved keywords are parsed as attributes
908    /// HTML gets annoying sometimes so we just accept them
909    #[test]
910    fn reserved_keywords() {
911        let _a: Attribute = parse2(quote! { for: "class" }).unwrap();
912        let _b: Attribute = parse2(quote! { type: "class" }).unwrap();
913    }
914}