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,
};
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct Attribute {
pub name: AttributeName,
pub colon: Option<Token![:]>,
pub value: AttributeValue,
pub comma: Option<Token![,]>,
pub dyn_idx: DynIdx,
pub el_name: Option<ElementName>,
}
impl Parse for Attribute {
fn parse(content: ParseStream) -> syn::Result<Self> {
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,
});
}
let name = match content.peek(LitStr) {
true => AttributeName::Custom(content.parse::<LitStr>()?),
false => AttributeName::BuiltIn(parse_raw_ident(content)?),
};
let colon = Some(content.parse::<Token![:]>()?);
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 {
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,
}
}
pub fn set_dyn_idx(&self, idx: usize) {
self.dyn_idx.set(idx);
}
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,
}
}
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 {
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 {
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 matches!(self.value, AttributeValue::Shorthand(_)) {
return true;
}
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
}
fn completion_hints(&self) -> TokenStream2 {
let Attribute {
name,
value,
comma,
el_name,
..
} = self;
if comma.is_some() {
return quote! {};
}
let (
Some(ElementName::Ident(el)),
AttributeName::BuiltIn(name),
AttributeValue::Shorthand(_),
) = (&el_name, &name, &value)
else {
return quote! {};
};
if name.to_string().starts_with("on") {
return quote! {};
}
quote! {
{
#[allow(dead_code)]
#[doc(hidden)]
mod __completions {
pub use super::dioxus_elements::#el::*;
pub use super::dioxus_elements::elements::completions::CompleteWithBraces::*;
fn ignore() {
#name;
}
}
}
}
}
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum AttributeName {
Spread(Token![..]),
BuiltIn(Ident),
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),
}
}
}
#[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 {
Shorthand(Ident),
AttrLiteral(HotLiteral),
EventTokens(PartialClosure),
IfExpr(IfAttributeValue),
AttrExpr(PartialExpr),
}
impl Parse for AttributeValue {
fn parse(content: ParseStream) -> syn::Result<Self> {
if content.peek(Token![if]) {
return Ok(Self::IfExpr(content.parse::<IfAttributeValue>()?));
}
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(),
}
}
}
#[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 {
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() {
Some(AttributeValue::IfExpr(else_value)) => {
current_if_value = else_value;
}
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());
}
}
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",
));
}
let stmt = &stmts[0];
match stmt {
syn::Stmt::Expr(exp, None) => {
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;
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) {
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 }
}
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",
));
}
let then_value = Self::parse_attribute_value_from_block(&if_expr.then_branch)?;
let else_value = match if_expr.else_branch.as_ref() {
Some((_, else_branch)) => {
let attribute_value = match else_branch.as_ref() {
Expr::Block(block) => Self::parse_attribute_value_from_block(&block.block)?,
_ => 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) {
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();
let _parsed: Attribute = parse2(quote! { "custom": false, }).unwrap();
let _parsed: Attribute = parse2(quote! { name: false, }).unwrap();
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(_)));
let _parsed: Attribute = parse2(quote! { name }).unwrap();
let _parsed: Attribute = parse2(quote! { name, }).unwrap();
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() {
let a: Attribute = parse2(quote! { class: "value1" }).unwrap();
let b: Attribute = parse2(quote! { class: "value1" }).unwrap();
assert_eq!(a, b);
let a: Attribute = parse2(quote! { class: var }).unwrap();
let b: Attribute = parse2(quote! { class: var }).unwrap();
assert_eq!(a, b);
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);
}
#[test]
fn reserved_keywords() {
let _a: Attribute = parse2(quote! { for: "class" }).unwrap();
let _b: Attribute = parse2(quote! { type: "class" }).unwrap();
}
}