1use 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#[derive(PartialEq, Eq, Clone, Debug, Hash)]
36pub struct Attribute {
37 pub name: AttributeName,
41
42 pub colon: Option<Token![:]>,
44
45 pub value: AttributeValue,
49
50 pub comma: Option<Token![,]>,
53
54 pub dyn_idx: DynIdx,
56
57 pub el_name: Option<ElementName>,
60}
61
62impl Parse for Attribute {
63 fn parse(content: ParseStream) -> syn::Result<Self> {
64 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 let name = match content.peek(LitStr) {
81 true => AttributeName::Custom(content.parse::<LitStr>()?),
82 false => AttributeName::BuiltIn(parse_raw_ident(content)?),
83 };
84
85 let colon = Some(content.parse::<Token![:]>()?);
87
88 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 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 pub fn set_dyn_idx(&self, idx: usize) {
123 self.dyn_idx.set(idx);
124 }
125
126 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 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 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 = █
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 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 matches!(self.value, AttributeValue::Shorthand(_)) {
308 return true;
309 }
310
311 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 fn completion_hints(&self) -> TokenStream2 {
328 let Attribute {
329 name,
330 value,
331 comma,
332 el_name,
333 ..
334 } = self;
335
336 if comma.is_some() {
338 return quote! {};
339 }
340
341 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 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 pub use super::dioxus_elements::#el::*;
365 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 BuiltIn(Ident),
382
383 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#[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 Shorthand(Ident),
448
449 AttrLiteral(HotLiteral),
455
456 EventTokens(PartialClosure),
462
463 IfExpr(IfAttributeValue),
470
471 AttrExpr(PartialExpr),
474}
475
476impl Parse for AttributeValue {
477 fn parse(content: ParseStream) -> syn::Result<Self> {
478 if content.peek(Token![if]) {
480 return Ok(Self::IfExpr(content.parse::<IfAttributeValue>()?));
481 }
482
483 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#[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 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 = ¤t_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 Some(AttributeValue::IfExpr(else_value)) => {
574 current_if_value = else_value;
575 }
576 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 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 let stmt = &stmts[0];
646
647 match stmt {
649 syn::Stmt::Expr(exp, None) => {
650 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 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 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 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 let then_value = Self::parse_attribute_value_from_block(&if_expr.then_branch)?;
752
753 let else_value = match if_expr.else_branch.as_ref() {
755 Some((_, else_branch)) => {
756 let attribute_value = match else_branch.as_ref() {
758 Expr::Block(block) => Self::parse_attribute_value_from_block(&block.block)?,
760 _ => 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 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 let _parsed: Attribute = parse2(quote! { "custom": false, }).unwrap();
804 let _parsed: Attribute = parse2(quote! { name: false, }).unwrap();
805
806 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 let _parsed: Attribute = parse2(quote! { name }).unwrap();
818 let _parsed: Attribute = parse2(quote! { name, }).unwrap();
819
820 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 let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
861 let b: Attribute = parse2(quote! { class: "value1" }).unwrap();
862 assert_eq!(a, b);
863
864 let a: Attribute = parse2(quote! { class: var }).unwrap();
866 let b: Attribute = parse2(quote! { class: var }).unwrap();
867 assert_eq!(a, b);
868
869 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 #[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}