1use 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#[derive(PartialEq, Eq, Clone, Debug, Hash)]
35pub struct Attribute {
36 pub name: AttributeName,
40
41 pub colon: Option<Token![:]>,
43
44 pub value: AttributeValue,
48
49 pub comma: Option<Token![,]>,
52
53 pub dyn_idx: DynIdx,
55
56 pub el_name: Option<ElementName>,
59}
60
61impl Parse for Attribute {
62 fn parse(content: ParseStream) -> syn::Result<Self> {
63 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 let name = match content.peek(LitStr) {
80 true => AttributeName::Custom(content.parse::<LitStr>()?),
81 false => AttributeName::BuiltIn(parse_raw_ident(content)?),
82 };
83
84 let colon = Some(content.parse::<Token![:]>()?);
86
87 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 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 pub fn set_dyn_idx(&self, idx: usize) {
122 self.dyn_idx.set(idx);
123 }
124
125 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 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 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 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 matches!(self.value, AttributeValue::Shorthand(_)) {
276 return true;
277 }
278
279 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 fn completion_hints(&self) -> TokenStream2 {
296 let Attribute {
297 name,
298 value,
299 comma,
300 el_name,
301 ..
302 } = self;
303
304 if comma.is_some() {
306 return quote! {};
307 }
308
309 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 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 pub use super::dioxus_elements::#el::*;
333 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 BuiltIn(Ident),
350
351 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#[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 Shorthand(Ident),
416
417 AttrLiteral(HotLiteral),
423
424 EventTokens(PartialClosure),
430
431 IfExpr(IfAttributeValue),
438
439 AttrExpr(PartialExpr),
442}
443
444impl Parse for AttributeValue {
445 fn parse(content: ParseStream) -> syn::Result<Self> {
446 if content.peek(Token![if]) {
448 return Ok(Self::IfExpr(content.parse::<IfAttributeValue>()?));
449 }
450
451 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#[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 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 = ¤t_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 Some(AttributeValue::IfExpr(else_value)) => {
541 current_if_value = else_value;
542 }
543 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 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 let stmt = &stmts[0];
611
612 match stmt {
614 syn::Stmt::Expr(exp, None) => {
615 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 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 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 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 let then_value = Self::parse_attribute_value_from_block(&if_expr.then_branch)?;
717
718 let else_value = match if_expr.else_branch.as_ref() {
720 Some((_, else_branch)) => {
721 let attribute_value = match else_branch.as_ref() {
723 Expr::Block(block) => Self::parse_attribute_value_from_block(&block.block)?,
725 _ => 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 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 let _parsed: Attribute = parse2(quote! { "custom": false, }).unwrap();
768 let _parsed: Attribute = parse2(quote! { name: false, }).unwrap();
769
770 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 let _parsed: Attribute = parse2(quote! { name }).unwrap();
782 let _parsed: Attribute = parse2(quote! { name, }).unwrap();
783
784 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 let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
825 let b: Attribute = parse2(quote! { class: "value1" }).unwrap();
826 assert_eq!(a, b);
827
828 let a: Attribute = parse2(quote! { class: var }).unwrap();
830 let b: Attribute = parse2(quote! { class: var }).unwrap();
831 assert_eq!(a, b);
832
833 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 #[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}