1
0

attribute.rs 29 KB

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