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