123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848 |
- //! Parser for the attribute shared both by elements and components
- //!
- //! ```rust, ignore
- //! rsx! {
- //! div {
- //! class: "my-class",
- //! onclick: move |_| println!("clicked")
- //! }
- //!
- //! Component {
- //! class: "my-class",
- //! onclick: move |_| println!("clicked")
- //! }
- //! }
- //! ```
- use super::literal::HotLiteral;
- use crate::{innerlude::*, partial_closure::PartialClosure};
- use proc_macro2::TokenStream as TokenStream2;
- use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
- use std::fmt::Display;
- use syn::{
- ext::IdentExt,
- parse::{Parse, ParseStream},
- parse_quote,
- spanned::Spanned,
- Block, Expr, ExprClosure, ExprIf, Ident, Lit, LitBool, LitFloat, LitInt, LitStr, Token,
- };
- /// A property value in the from of a `name: value` pair with an optional comma.
- /// Note that the colon and value are optional in the case of shorthand attributes. We keep them around
- /// to support "lossless" parsing in case that ever might be useful.
- #[derive(PartialEq, Eq, Clone, Debug, Hash)]
- pub struct Attribute {
- /// The name of the attribute (ident or custom)
- ///
- /// IE `class` or `onclick`
- pub name: AttributeName,
- /// The colon that separates the name and value - keep this for lossless parsing
- pub colon: Option<Token![:]>,
- /// The value of the attribute
- ///
- /// IE `class="my-class"` or `onclick: move |_| println!("clicked")`
- pub value: AttributeValue,
- /// The comma that separates this attribute from the next one
- /// Used for more accurate completions
- pub comma: Option<Token![,]>,
- /// The dynamic index of this attribute - used by the template system
- pub dyn_idx: DynIdx,
- /// The element name of this attribute if it is bound to an element.
- /// When parsed for components or freestanding, this will be None
- pub el_name: Option<ElementName>,
- }
- impl Parse for Attribute {
- fn parse(content: ParseStream) -> syn::Result<Self> {
- // if there's an ident not followed by a colon, it's a shorthand attribute
- if content.peek(Ident::peek_any) && !content.peek2(Token![:]) {
- let ident = parse_raw_ident(content)?;
- let comma = content.parse().ok();
- return Ok(Attribute {
- name: AttributeName::BuiltIn(ident.clone()),
- colon: None,
- value: AttributeValue::Shorthand(ident),
- comma,
- dyn_idx: DynIdx::default(),
- el_name: None,
- });
- }
- // Parse the name as either a known or custom attribute
- let name = match content.peek(LitStr) {
- true => AttributeName::Custom(content.parse::<LitStr>()?),
- false => AttributeName::BuiltIn(parse_raw_ident(content)?),
- };
- // Ensure there's a colon
- let colon = Some(content.parse::<Token![:]>()?);
- // todo: make this cleaner please
- // if statements in attributes get automatic closing in some cases
- // we shouldn't be handling it any differently.
- let value = AttributeValue::parse(content)?;
- let comma = content.parse::<Token![,]>().ok();
- let attr = Attribute {
- name,
- value,
- colon,
- comma,
- dyn_idx: DynIdx::default(),
- el_name: None,
- };
- Ok(attr)
- }
- }
- impl Attribute {
- /// Create a new attribute from a name and value
- pub fn from_raw(name: AttributeName, value: AttributeValue) -> Self {
- Self {
- name,
- colon: Default::default(),
- value,
- comma: Default::default(),
- dyn_idx: Default::default(),
- el_name: None,
- }
- }
- /// Set the dynamic index of this attribute
- pub fn set_dyn_idx(&self, idx: usize) {
- self.dyn_idx.set(idx);
- }
- /// Get the dynamic index of this attribute
- pub fn get_dyn_idx(&self) -> usize {
- self.dyn_idx.get()
- }
- pub fn span(&self) -> proc_macro2::Span {
- self.name.span()
- }
- pub fn as_lit(&self) -> Option<&HotLiteral> {
- match &self.value {
- AttributeValue::AttrLiteral(lit) => Some(lit),
- _ => None,
- }
- }
- /// Run this closure against the attribute if it's hotreloadable
- pub fn with_literal(&self, f: impl FnOnce(&HotLiteral)) {
- if let AttributeValue::AttrLiteral(ifmt) = &self.value {
- f(ifmt);
- }
- }
- pub fn ifmt(&self) -> Option<&IfmtInput> {
- match &self.value {
- AttributeValue::AttrLiteral(HotLiteral::Fmted(input)) => Some(input),
- _ => None,
- }
- }
- pub fn as_static_str_literal(&self) -> Option<(&AttributeName, &IfmtInput)> {
- match &self.value {
- AttributeValue::AttrLiteral(lit) => match &lit {
- HotLiteral::Fmted(input) if input.is_static() => Some((&self.name, input)),
- _ => None,
- },
- _ => None,
- }
- }
- pub fn is_static_str_literal(&self) -> bool {
- self.as_static_str_literal().is_some()
- }
- pub fn rendered_as_dynamic_attr(&self) -> TokenStream2 {
- // Shortcut out with spreads
- if let AttributeName::Spread(_) = self.name {
- let AttributeValue::AttrExpr(expr) = &self.value else {
- unreachable!("Spread attributes should always be expressions")
- };
- return quote! { {#expr}.into_boxed_slice() };
- }
- let el_name = self
- .el_name
- .as_ref()
- .expect("el_name rendered as a dynamic attribute should always have an el_name set");
- let ns = |name: &AttributeName| match (el_name, name) {
- (ElementName::Ident(i), AttributeName::BuiltIn(_)) => {
- quote! { dioxus_elements::#i::#name.1 }
- }
- _ => quote! { None },
- };
- let volatile = |name: &AttributeName| match (el_name, name) {
- (ElementName::Ident(i), AttributeName::BuiltIn(_)) => {
- quote! { dioxus_elements::#i::#name.2 }
- }
- _ => quote! { false },
- };
- let attribute = |name: &AttributeName| match name {
- AttributeName::BuiltIn(name) => match el_name {
- ElementName::Ident(_) => quote! { dioxus_elements::#el_name::#name.0 },
- ElementName::Custom(_) => {
- let as_string = name.to_string();
- quote!(#as_string)
- }
- },
- AttributeName::Custom(s) => quote! { #s },
- AttributeName::Spread(_) => unreachable!("Spread attributes are handled elsewhere"),
- };
- let attribute = {
- let value = &self.value;
- let name = &self.name;
- let is_not_event = !self.name.is_likely_event();
- match &self.value {
- AttributeValue::AttrLiteral(_)
- | AttributeValue::AttrExpr(_)
- | AttributeValue::Shorthand(_)
- | AttributeValue::IfExpr { .. }
- if is_not_event =>
- {
- let name = &self.name;
- let ns = ns(name);
- let volatile = volatile(name);
- let attribute = attribute(name);
- let value = quote! { #value };
- quote! {
- dioxus_core::Attribute::new(
- #attribute,
- #value,
- #ns,
- #volatile
- )
- }
- }
- AttributeValue::EventTokens(tokens) => match &self.name {
- AttributeName::BuiltIn(name) => {
- let event_tokens_is_closure =
- syn::parse2::<ExprClosure>(tokens.to_token_stream()).is_ok();
- let function_name =
- quote_spanned! { tokens.span() => dioxus_elements::events::#name };
- let function = if event_tokens_is_closure {
- // If we see an explicit closure, we can call the `call_with_explicit_closure` version of the event for better type inference
- quote_spanned! { tokens.span() => #function_name::call_with_explicit_closure }
- } else {
- function_name
- };
- quote_spanned! { tokens.span() =>
- #function(#tokens)
- }
- }
- AttributeName::Custom(_) => unreachable!("Handled elsewhere in the macro"),
- AttributeName::Spread(_) => unreachable!("Handled elsewhere in the macro"),
- },
- _ => {
- quote_spanned! { value.span() => dioxus_elements::events::#name(#value) }
- }
- }
- };
- let completion_hints = self.completion_hints();
- quote! {
- Box::new([
- {
- #completion_hints
- #attribute
- }
- ])
- }
- .to_token_stream()
- }
- pub fn can_be_shorthand(&self) -> bool {
- // If it's a shorthand...
- if matches!(self.value, AttributeValue::Shorthand(_)) {
- return true;
- }
- // Or if it is a builtin attribute with a single ident value
- if let (AttributeName::BuiltIn(name), AttributeValue::AttrExpr(expr)) =
- (&self.name, &self.value)
- {
- if let Ok(Expr::Path(path)) = expr.as_expr() {
- if path.path.get_ident() == Some(name) {
- return true;
- }
- }
- }
- false
- }
- /// If this is the last attribute of an element and it doesn't have a tailing comma,
- /// we add hints so that rust analyzer completes it either as an attribute or element
- fn completion_hints(&self) -> TokenStream2 {
- let Attribute {
- name,
- value,
- comma,
- el_name,
- ..
- } = self;
- // If there is a trailing comma, rust analyzer does a good job of completing the attribute by itself
- if comma.is_some() {
- return quote! {};
- }
- // Only add hints if the attribute is:
- // - a built in attribute (not a literal)
- // - an build in element (not a custom element)
- // - a shorthand attribute
- let (
- Some(ElementName::Ident(el)),
- AttributeName::BuiltIn(name),
- AttributeValue::Shorthand(_),
- ) = (&el_name, &name, &value)
- else {
- return quote! {};
- };
- // 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
- if name.to_string().starts_with("on") {
- return quote! {};
- }
- quote! {
- {
- #[allow(dead_code)]
- #[doc(hidden)]
- mod __completions {
- // Autocomplete as an attribute
- pub use super::dioxus_elements::#el::*;
- // Autocomplete as an element
- pub use super::dioxus_elements::elements::completions::CompleteWithBraces::*;
- fn ignore() {
- #name;
- }
- }
- }
- }
- }
- }
- #[derive(PartialEq, Eq, Clone, Debug, Hash)]
- pub enum AttributeName {
- Spread(Token![..]),
- /// an attribute in the form of `name: value`
- BuiltIn(Ident),
- /// an attribute in the form of `"name": value` - notice that the name is a string literal
- /// this is to allow custom attributes in the case of missing built-in attributes
- ///
- /// we might want to change this one day to be ticked or something and simply a boolean
- Custom(LitStr),
- }
- impl AttributeName {
- pub fn is_likely_event(&self) -> bool {
- matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on"))
- }
- pub fn is_likely_key(&self) -> bool {
- matches!(self, Self::BuiltIn(ident) if ident == "key")
- }
- pub fn span(&self) -> proc_macro2::Span {
- match self {
- Self::Custom(lit) => lit.span(),
- Self::BuiltIn(ident) => ident.span(),
- Self::Spread(dots) => dots.span(),
- }
- }
- }
- impl Display for AttributeName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Custom(lit) => write!(f, "{}", lit.value()),
- Self::BuiltIn(ident) => write!(f, "{}", ident),
- Self::Spread(_) => write!(f, ".."),
- }
- }
- }
- impl ToTokens for AttributeName {
- fn to_tokens(&self, tokens: &mut TokenStream2) {
- match self {
- Self::Custom(lit) => lit.to_tokens(tokens),
- Self::BuiltIn(ident) => ident.to_tokens(tokens),
- Self::Spread(dots) => dots.to_tokens(tokens),
- }
- }
- }
- // ..spread attribute
- #[derive(PartialEq, Eq, Clone, Debug, Hash)]
- pub struct Spread {
- pub dots: Token![..],
- pub expr: Expr,
- pub dyn_idx: DynIdx,
- pub comma: Option<Token![,]>,
- }
- impl Spread {
- pub fn span(&self) -> proc_macro2::Span {
- self.dots.span()
- }
- }
- #[derive(PartialEq, Eq, Clone, Debug, Hash)]
- pub enum AttributeValue {
- /// Just a regular shorthand attribute - an ident. Makes our parsing a bit more opaque.
- /// attribute,
- Shorthand(Ident),
- /// Any attribute that's a literal. These get hotreloading super powers
- ///
- /// attribute: "value"
- /// attribute: bool,
- /// attribute: 1,
- AttrLiteral(HotLiteral),
- /// A series of tokens that represent an event handler
- ///
- /// We use a special type here so we can get autocomplete in the closure using partial expansion.
- /// We also do some extra wrapping for improved type hinting since rust sometimes has trouble with
- /// generics and closures.
- EventTokens(PartialClosure),
- /// Conditional expression
- ///
- /// attribute: if bool { "value" } else if bool { "other value" } else { "default value" }
- ///
- /// Currently these don't get hotreloading super powers, but they could, depending on how far
- /// we want to go with it
- IfExpr(IfAttributeValue),
- /// attribute: some_expr
- /// attribute: {some_expr} ?
- AttrExpr(PartialExpr),
- }
- impl Parse for AttributeValue {
- fn parse(content: ParseStream) -> syn::Result<Self> {
- // Attempt to parse the unterminated if statement
- if content.peek(Token![if]) {
- return Ok(Self::IfExpr(content.parse::<IfAttributeValue>()?));
- }
- // Use the move and/or bars as an indicator that we have an event handler
- if content.peek(Token![move]) || content.peek(Token![|]) {
- let value = content.parse()?;
- return Ok(AttributeValue::EventTokens(value));
- }
- if content.peek(LitStr)
- || content.peek(LitBool)
- || content.peek(LitFloat)
- || content.peek(LitInt)
- {
- let fork = content.fork();
- _ = fork.parse::<Lit>().unwrap();
- if content.peek2(Token![,]) || fork.is_empty() {
- let value = content.parse()?;
- return Ok(AttributeValue::AttrLiteral(value));
- }
- }
- let value = content.parse::<PartialExpr>()?;
- Ok(AttributeValue::AttrExpr(value))
- }
- }
- impl ToTokens for AttributeValue {
- fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
- match self {
- Self::Shorthand(ident) => ident.to_tokens(tokens),
- Self::AttrLiteral(ifmt) => ifmt.to_tokens(tokens),
- Self::IfExpr(if_expr) => if_expr.to_tokens(tokens),
- Self::AttrExpr(expr) => expr.to_tokens(tokens),
- Self::EventTokens(closure) => closure.to_tokens(tokens),
- }
- }
- }
- impl AttributeValue {
- pub fn span(&self) -> proc_macro2::Span {
- match self {
- Self::Shorthand(ident) => ident.span(),
- Self::AttrLiteral(ifmt) => ifmt.span(),
- Self::IfExpr(if_expr) => if_expr.span(),
- Self::AttrExpr(expr) => expr.span(),
- Self::EventTokens(closure) => closure.span(),
- }
- }
- }
- /// A if else chain attribute value
- #[derive(PartialEq, Eq, Clone, Debug, Hash)]
- pub struct IfAttributeValue {
- pub condition: Expr,
- pub then_value: Box<AttributeValue>,
- pub else_value: Option<Box<AttributeValue>>,
- }
- impl IfAttributeValue {
- /// Convert the if expression to an expression that returns a string. If the unterminated case is hit, it returns an empty string
- pub(crate) fn quote_as_string(&self, diagnostics: &mut Diagnostics) -> Expr {
- let mut expression = quote! {};
- let mut current_if_value = self;
- let mut non_string_diagnostic = |span: proc_macro2::Span| -> Expr {
- Element::add_merging_non_string_diagnostic(diagnostics, span);
- parse_quote! { ::std::string::String::new() }
- };
- loop {
- let AttributeValue::AttrLiteral(lit) = current_if_value.then_value.as_ref() else {
- return non_string_diagnostic(current_if_value.span());
- };
- let HotLiteral::Fmted(HotReloadFormattedSegment {
- formatted_input: new,
- ..
- }) = &lit
- else {
- return non_string_diagnostic(current_if_value.span());
- };
- let condition = ¤t_if_value.condition;
- expression.extend(quote! {
- if #condition {
- #new.to_string()
- } else
- });
- match current_if_value.else_value.as_deref() {
- // If the else value is another if expression, then we need to continue the loop
- Some(AttributeValue::IfExpr(else_value)) => {
- current_if_value = else_value;
- }
- // If the else value is a literal, then we need to append it to the expression and break
- Some(AttributeValue::AttrLiteral(lit)) => {
- if let HotLiteral::Fmted(new) = &lit {
- let fmted = &new.formatted_input;
- expression.extend(quote! { { #fmted.to_string() } });
- break;
- } else {
- return non_string_diagnostic(current_if_value.span());
- }
- }
- // If it is the end of the if expression without an else, then we need to append the default value and break
- None => {
- expression.extend(quote! { { ::std::string::String::new() } });
- break;
- }
- _ => {
- return non_string_diagnostic(current_if_value.else_value.span());
- }
- }
- }
- parse_quote! {
- {
- #expression
- }
- }
- }
- fn span(&self) -> proc_macro2::Span {
- self.then_value.span()
- }
- fn is_terminated(&self) -> bool {
- match &self.else_value {
- Some(attribute) => match attribute.as_ref() {
- AttributeValue::IfExpr(if_expr) => if_expr.is_terminated(),
- _ => true,
- },
- None => false,
- }
- }
- fn contains_expression(&self) -> bool {
- if let AttributeValue::AttrExpr(_) = &*self.then_value {
- return true;
- }
- match &self.else_value {
- Some(attribute) => match attribute.as_ref() {
- AttributeValue::IfExpr(if_expr) => if_expr.is_terminated(),
- AttributeValue::AttrExpr(_) => true,
- _ => false,
- },
- None => false,
- }
- }
- fn parse_attribute_value_from_block(block: &Block) -> syn::Result<Box<AttributeValue>> {
- let stmts = &block.stmts;
- if stmts.len() != 1 {
- return Err(syn::Error::new(
- block.span(),
- "Expected a single statement in the if block",
- ));
- }
- // either an ifmt or an expr in the block
- let stmt = &stmts[0];
- // Either it's a valid ifmt or an expression
- match stmt {
- syn::Stmt::Expr(exp, None) => {
- // Try parsing the statement as an IfmtInput by passing it through tokens
- let value: Result<HotLiteral, syn::Error> = syn::parse2(quote! { #exp });
- Ok(match value {
- Ok(res) => Box::new(AttributeValue::AttrLiteral(res)),
- Err(_) => Box::new(AttributeValue::AttrExpr(PartialExpr::from_expr(exp))),
- })
- }
- _ => Err(syn::Error::new(stmt.span(), "Expected an expression")),
- }
- }
- fn to_tokens_with_terminated(
- &self,
- tokens: &mut TokenStream2,
- terminated: bool,
- contains_expression: bool,
- ) {
- let IfAttributeValue {
- condition,
- then_value,
- else_value,
- } = self;
- // Quote an attribute value and convert the value to a string if it is formatted
- // We always quote formatted segments as strings inside if statements so they have a consistent type
- // This fixes https://github.com/DioxusLabs/dioxus/issues/2997
- fn quote_attribute_value_string(
- value: &AttributeValue,
- contains_expression: bool,
- ) -> TokenStream2 {
- if let AttributeValue::AttrLiteral(HotLiteral::Fmted(fmted)) = value {
- if let Some(str) = fmted.to_static().filter(|_| contains_expression) {
- // If this is actually a static string, the user may be using a static string expression in another branch
- // use into to convert the string to whatever the other branch is using
- quote! {
- {
- #[allow(clippy::useless_conversion)]
- #str.into()
- }
- }
- } else {
- quote! { #value.to_string() }
- }
- } else {
- value.to_token_stream()
- }
- }
- let then_value = quote_attribute_value_string(then_value, terminated);
- let then_value = if terminated {
- quote! { #then_value }
- }
- // Otherwise we need to return an Option and a None if the else value is None
- else {
- quote! { Some(#then_value) }
- };
- let else_value = match else_value.as_deref() {
- Some(AttributeValue::IfExpr(else_value)) => {
- let mut tokens = TokenStream2::new();
- else_value.to_tokens_with_terminated(&mut tokens, terminated, contains_expression);
- tokens
- }
- Some(other) => {
- let other = quote_attribute_value_string(other, contains_expression);
- if terminated {
- quote! { #other }
- } else {
- quote! { Some(#other) }
- }
- }
- None => quote! { None },
- };
- tokens.append_all(quote! {
- {
- if #condition {
- #then_value
- } else {
- #else_value
- }
- }
- });
- }
- }
- impl Parse for IfAttributeValue {
- fn parse(input: ParseStream) -> syn::Result<Self> {
- let if_expr = input.parse::<ExprIf>()?;
- let stmts = &if_expr.then_branch.stmts;
- if stmts.len() != 1 {
- return Err(syn::Error::new(
- if_expr.then_branch.span(),
- "Expected a single statement in the if block",
- ));
- }
- // Parse the then branch into a single attribute value
- let then_value = Self::parse_attribute_value_from_block(&if_expr.then_branch)?;
- // If there's an else branch, parse it as a single attribute value or an if expression
- let else_value = match if_expr.else_branch.as_ref() {
- Some((_, else_branch)) => {
- // The else branch if either a block or another if expression
- let attribute_value = match else_branch.as_ref() {
- // If it is a block, then the else is terminated
- Expr::Block(block) => Self::parse_attribute_value_from_block(&block.block)?,
- // Otherwise try to parse it as an if expression
- _ => Box::new(syn::parse2(quote! { #else_branch })?),
- };
- Some(attribute_value)
- }
- None => None,
- };
- Ok(Self {
- condition: *if_expr.cond,
- then_value,
- else_value,
- })
- }
- }
- impl ToTokens for IfAttributeValue {
- fn to_tokens(&self, tokens: &mut TokenStream2) {
- // If the if expression is terminated, we can just return the then value
- let terminated = self.is_terminated();
- let contains_expression = self.contains_expression();
- self.to_tokens_with_terminated(tokens, terminated, contains_expression)
- }
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- use quote::quote;
- use syn::parse2;
- #[test]
- fn parse_attrs() {
- let _parsed: Attribute = parse2(quote! { name: "value" }).unwrap();
- let _parsed: Attribute = parse2(quote! { name: value }).unwrap();
- let _parsed: Attribute = parse2(quote! { name: "value {fmt}" }).unwrap();
- let _parsed: Attribute = parse2(quote! { name: 123 }).unwrap();
- let _parsed: Attribute = parse2(quote! { name: false }).unwrap();
- let _parsed: Attribute = parse2(quote! { "custom": false }).unwrap();
- let _parsed: Attribute = parse2(quote! { prop: "blah".to_string() }).unwrap();
- // with commas
- let _parsed: Attribute = parse2(quote! { "custom": false, }).unwrap();
- let _parsed: Attribute = parse2(quote! { name: false, }).unwrap();
- // with if chains
- let parsed: Attribute = parse2(quote! { name: if true { "value" } }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
- let parsed: Attribute =
- parse2(quote! { name: if true { "value" } else { "other" } }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
- let parsed: Attribute =
- parse2(quote! { name: if true { "value" } else if false { "other" } }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::IfExpr(_)));
- // with shorthand
- let _parsed: Attribute = parse2(quote! { name }).unwrap();
- let _parsed: Attribute = parse2(quote! { name, }).unwrap();
- // Events - make sure they get partial expansion
- let parsed: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
- let parsed: Attribute = parse2(quote! { onclick: |e| { "value" } }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
- let parsed: Attribute = parse2(quote! { onclick: |e| { value. } }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
- let parsed: Attribute = parse2(quote! { onclick: move |e| { value. } }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
- let parsed: Attribute = parse2(quote! { onclick: move |e| value }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
- let parsed: Attribute = parse2(quote! { onclick: |e| value, }).unwrap();
- assert!(matches!(parsed.value, AttributeValue::EventTokens(_)));
- }
- #[test]
- fn merge_attrs() {
- let _a: Attribute = parse2(quote! { class: "value1" }).unwrap();
- let _b: Attribute = parse2(quote! { class: "value2" }).unwrap();
- let _b: Attribute = parse2(quote! { class: "value2 {something}" }).unwrap();
- let _b: Attribute = parse2(quote! { class: if value { "other thing" } }).unwrap();
- let _b: Attribute = parse2(quote! { class: if value { some_expr } }).unwrap();
- let _b: Attribute = parse2(quote! { class: if value { "some_expr" } }).unwrap();
- dbg!(_b);
- }
- #[test]
- fn static_literals() {
- let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
- let b: Attribute = parse2(quote! { class: "value {some}" }).unwrap();
- assert!(a.is_static_str_literal());
- assert!(!b.is_static_str_literal());
- }
- #[test]
- fn partial_eqs() {
- // Basics
- let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
- let b: Attribute = parse2(quote! { class: "value1" }).unwrap();
- assert_eq!(a, b);
- // Exprs
- let a: Attribute = parse2(quote! { class: var }).unwrap();
- let b: Attribute = parse2(quote! { class: var }).unwrap();
- assert_eq!(a, b);
- // Events
- let a: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
- let b: Attribute = parse2(quote! { onclick: |e| {} }).unwrap();
- let c: Attribute = parse2(quote! { onclick: move |e| {} }).unwrap();
- assert_eq!(a, b);
- assert_ne!(a, c);
- }
- /// Make sure reserved keywords are parsed as attributes
- /// HTML gets annoying sometimes so we just accept them
- #[test]
- fn reserved_keywords() {
- let _a: Attribute = parse2(quote! { for: "class" }).unwrap();
- let _b: Attribute = parse2(quote! { type: "class" }).unwrap();
- }
- }
|