From 8c47dfaf78a734460d5335897fb7e68a65bb4144 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 14 Sep 2023 14:08:39 -0500 Subject: [PATCH 01/11] clean up attribute parsing --- packages/autofmt/src/element.rs | 56 +++++----- packages/autofmt/src/writer.rs | 32 +++--- packages/rsx-rosetta/src/lib.rs | 26 +++-- packages/rsx/Cargo.toml | 2 + packages/rsx/src/attribute.rs | 165 ++++++++++++++++++++++++++++ packages/rsx/src/element.rs | 187 +++++--------------------------- packages/rsx/src/errors.rs | 4 +- packages/rsx/src/lib.rs | 67 ++++-------- 8 files changed, 278 insertions(+), 261 deletions(-) create mode 100644 packages/rsx/src/attribute.rs diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index dbc336401..971f4447e 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -209,12 +209,25 @@ impl Writer<'_> { Ok(()) } - fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { - match &attr.attr { - ElementAttr::AttrText { name, value } => { - write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?; + fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result { + match attr { + ElementAttrName::BuiltIn(name) => { + write!(self.out, "{}", name)?; } - ElementAttr::AttrExpression { name, value } => { + ElementAttrName::Custom(name) => { + write!(self.out, "\"{}\"", name.to_token_stream())?; + } + } + + Ok(()) + } + + fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result { + match value { + ElementAttrValue::AttrLiteral(value) => { + write!(self.out, "{value}", value = ifmt_to_string(value))?; + } + ElementAttrValue::AttrExpr(value) => { let out = prettyplease::unparse_expr(value); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); @@ -222,9 +235,9 @@ impl Writer<'_> { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.out, "{name}: {first}")?; + write!(self.out, "{first}")?; } else { - writeln!(self.out, "{name}: {first}")?; + writeln!(self.out, "{first}")?; while let Some(line) = lines.next() { self.out.indented_tab()?; @@ -237,22 +250,7 @@ impl Writer<'_> { } } } - - ElementAttr::CustomAttrText { name, value } => { - write!( - self.out, - "{name}: {value}", - name = name.to_token_stream(), - value = ifmt_to_string(value) - )?; - } - - ElementAttr::CustomAttrExpression { name, value } => { - let out = prettyplease::unparse_expr(value); - write!(self.out, "{}: {}", name.to_token_stream(), out)?; - } - - ElementAttr::EventTokens { name, tokens } => { + ElementAttrValue::EventTokens(tokens) => { let out = self.retrieve_formatted_expr(tokens).to_string(); let mut lines = out.split('\n').peekable(); @@ -261,9 +259,9 @@ impl Writer<'_> { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.out, "{name}: {first}")?; + write!(self.out, "{first}")?; } else { - writeln!(self.out, "{name}: {first}")?; + writeln!(self.out, "{first}")?; while let Some(line) = lines.next() { self.out.indented_tab()?; @@ -281,6 +279,14 @@ impl Writer<'_> { Ok(()) } + fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { + self.write_attribute_name(&attr.attr.name)?; + write!(self.out, ": ")?; + self.write_attribute_value(&attr.attr.value)?; + + Ok(()) + } + // make sure the comments are actually relevant to this element. // test by making sure this element is the primary element on this line pub fn current_span_is_primary(&self, location: Span) -> bool { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index 59f135592..c96e96c93 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,4 +1,4 @@ -use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop}; +use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; use std::{ @@ -146,20 +146,18 @@ impl<'a> Writer<'a> { } } - total += match &attr.attr { - ElementAttr::AttrText { value, name } => { - ifmt_to_string(value).len() + name.span().line_length() + 6 + total += match &attr.attr.name { + dioxus_rsx::ElementAttrName::BuiltIn(name) => { + let name = name.to_string(); + name.len() } - ElementAttr::AttrExpression { name, value } => { - value.span().line_length() + name.span().line_length() + 6 - } - ElementAttr::CustomAttrText { value, name } => { - ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6 - } - ElementAttr::CustomAttrExpression { name, value } => { - name.to_token_stream().to_string().len() + value.span().line_length() + 6 - } - ElementAttr::EventTokens { tokens, name } => { + dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, + }; + + total += match &attr.attr.value { + ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), + ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), + ElementAttrValue::EventTokens(tokens) => { let location = Location::new(tokens.span().start()); let len = if let std::collections::hash_map::Entry::Vacant(e) = @@ -177,9 +175,11 @@ impl<'a> Writer<'a> { self.cached_formats[&location].len() }; - len + name.span().line_length() + 6 + len } }; + + total += 6; } total @@ -218,7 +218,7 @@ impl<'a> Writer<'a> { } } -trait SpanLength { +pub(crate) trait SpanLength { fn line_length(&self) -> usize; } impl SpanLength for Span { diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 2ceb5f038..19b1d5375 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -36,9 +36,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option { ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - value: ifmt_from_text(value.as_deref().unwrap_or("false")), - name: ident, + attr: ElementAttr { + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text( + value.as_deref().unwrap_or("false"), + )), + name: dioxus_rsx::ElementAttrName::BuiltIn(ident), }, } }) @@ -48,9 +50,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option { if !class.is_empty() { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name: Ident::new("class", Span::call_site()), - value: ifmt_from_text(&class), + attr: ElementAttr { + name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( + "class", + Span::call_site(), + )), + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)), }, }); } @@ -58,9 +63,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option { if let Some(id) = &el.id { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name: Ident::new("id", Span::call_site()), - value: ifmt_from_text(id), + attr: ElementAttr { + name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( + "id", + Span::call_site(), + )), + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)), }, }); } diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index bc866acb7..0b7b23e98 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -22,5 +22,7 @@ internment = { version = "0.7.0", optional = true } krates = { version = "0.12.6", optional = true } [features] +default = ["html"] hot_reload = ["krates", "internment"] serde = ["dep:serde"] +html = [] diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs new file mode 100644 index 000000000..76cf55a01 --- /dev/null +++ b/packages/rsx/src/attribute.rs @@ -0,0 +1,165 @@ +use std::fmt::{Display, Formatter}; + +use super::*; + +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Expr, Ident, LitStr, Result, Token, +}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct ElementAttrNamed { + pub el_name: ElementName, + pub attr: ElementAttr, +} + +impl ElementAttrNamed { + pub(crate) fn try_combine(&self, other: Self) -> Option { + if self.el_name == other.el_name && self.attr.name == other.attr.name { + if let Some(separator) = todo!() { + return Some(ElementAttrNamed { + el_name: self.el_name.clone(), + attr: ElementAttr { + name: self.attr.name.clone(), + value: self.attr.value.combine(separator, other.attr.value), + }, + }); + } + } + None + } +} + +impl ToTokens for ElementAttrNamed { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ElementAttrNamed { el_name, attr } = self; + + let ns = |name| match el_name { + ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 }, + ElementName::Custom(_) => quote! { None }, + }; + let volitile = |name| match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.2 }, + ElementName::Custom(_) => quote! { false }, + }; + let attribute = |name: &ElementAttrName| match name { + ElementAttrName::BuiltIn(name) => match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.0 }, + ElementName::Custom(_) => { + let as_string = name.to_string(); + quote!(#as_string) + } + }, + ElementAttrName::Custom(s) => quote! { #s }, + }; + + let attribute = { + match &attr.value { + ElementAttrValue::AttrLiteral(_) | ElementAttrValue::AttrExpr(_) => { + let name = &self.attr.name; + let ns = ns(name); + let volitile = volitile(name); + let attribute = attribute(name); + let value = match &self.attr.value { + ElementAttrValue::AttrLiteral(lit) => quote! { #lit }, + ElementAttrValue::AttrExpr(expr) => quote! { #expr }, + _ => unreachable!(), + }; + quote! { + __cx.attr( + #attribute, + #value, + #ns, + #volitile + ) + } + } + ElementAttrValue::EventTokens(tokens) => match &self.attr.name { + ElementAttrName::BuiltIn(name) => { + quote! { + dioxus_elements::events::#name(__cx, #tokens) + } + } + ElementAttrName::Custom(_) => todo!(), + }, + } + }; + + tokens.append_all(attribute); + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct ElementAttr { + pub name: ElementAttrName, + pub value: ElementAttrValue, +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrValue { + /// attribute: "value" + AttrLiteral(IfmtInput), + /// attribute: true + AttrExpr(Expr), + /// onclick: move |_| {} + EventTokens(Expr), +} + +impl ElementAttrValue { + fn combine(&self, separator: &str, other: Self) -> Self { + todo!() + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrName { + BuiltIn(Ident), + Custom(LitStr), +} + +impl ElementAttrName { + pub fn start(&self) -> Span { + match self { + ElementAttrName::BuiltIn(i) => i.span(), + ElementAttrName::Custom(s) => s.span(), + } + } +} + +impl ToTokens for ElementAttrName { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementAttrName::BuiltIn(i) => tokens.append_all(quote! { #i }), + ElementAttrName::Custom(s) => tokens.append_all(quote! { #s }), + } + } +} + +impl Display for ElementAttrName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ElementAttrName::BuiltIn(i) => write!(f, "{}", i), + ElementAttrName::Custom(s) => write!(f, "{}", s.value()), + } + } +} + +impl ElementAttr { + pub fn start(&self) -> Span { + self.name.start() + } + + pub fn is_expr(&self) -> bool { + matches!( + self, + ElementAttr { + value: ElementAttrValue::AttrExpr(_) | ElementAttrValue::EventTokens(_), + .. + } + ) + } +} diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 8f45d91ff..19557edd2 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -8,7 +8,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, spanned::Spanned, - Error, Expr, Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -34,7 +34,6 @@ impl Parse for Element { let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; - let mut _el_ref = None; // parse fields with commas // break when we don't get this pattern anymore @@ -49,19 +48,20 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) { + let value = if content.peek(LitStr) { let value = content.parse()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); + ElementAttrValue::AttrLiteral(value) } else { let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); - } + ElementAttrValue::AttrExpr(value) + }; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::Custom(name), + value, + }, + }); if content.is_empty() { break; @@ -86,9 +86,9 @@ impl Parse for Element { if name_str.starts_with("on") { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value: ElementAttrValue::EventTokens(content.parse()?), }, }); } else { @@ -96,26 +96,21 @@ impl Parse for Element { "key" => { key = Some(content.parse()?); } - "classes" => todo!("custom class list not supported yet"), - // "namespace" => todo!("custom namespace not supported yet"), - "node_ref" => { - _el_ref = Some(content.parse::()?); - } _ => { if content.peek(LitStr) { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name, - value: content.parse()?, + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value: ElementAttrValue::AttrLiteral(content.parse()?), }, }); } else { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrExpression { - name, - value: content.parse()?, + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value: ElementAttrValue::AttrExpr(content.parse()?), }, }); } @@ -137,6 +132,9 @@ impl Parse for Element { break; } + // Deduplicate any attributes that can be combined + // For example, if there are two `class` attributes, combine them into one + while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { attr_after_element!(content.span()); @@ -177,12 +175,12 @@ impl ToTokens for Element { let listeners = self .attributes .iter() - .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); let attr = self .attributes .iter() - .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); tokens.append_all(quote! { __cx.element( @@ -264,136 +262,3 @@ impl ToTokens for ElementName { } } } - -#[derive(PartialEq, Eq, Clone, Debug, Hash)] -pub enum ElementAttr { - /// `attribute: "value"` - AttrText { name: Ident, value: IfmtInput }, - - /// `attribute: true` - AttrExpression { name: Ident, value: Expr }, - - /// `"attribute": "value"` - CustomAttrText { name: LitStr, value: IfmtInput }, - - /// `"attribute": true` - CustomAttrExpression { name: LitStr, value: Expr }, - - // /// onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, - /// onclick: {} - EventTokens { name: Ident, tokens: Expr }, -} - -impl ElementAttr { - pub fn start(&self) -> Span { - match self { - ElementAttr::AttrText { name, .. } => name.span(), - ElementAttr::AttrExpression { name, .. } => name.span(), - ElementAttr::CustomAttrText { name, .. } => name.span(), - ElementAttr::CustomAttrExpression { name, .. } => name.span(), - ElementAttr::EventTokens { name, .. } => name.span(), - } - } - - pub fn is_expr(&self) -> bool { - matches!( - self, - ElementAttr::AttrExpression { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } - ) - } -} - -#[derive(PartialEq, Eq, Clone, Debug, Hash)] -pub struct ElementAttrNamed { - pub el_name: ElementName, - pub attr: ElementAttr, -} - -impl ToTokens for ElementAttrNamed { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ElementAttrNamed { el_name, attr } = self; - - let ns = |name| match el_name { - ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 }, - ElementName::Custom(_) => quote! { None }, - }; - let volitile = |name| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.2 }, - ElementName::Custom(_) => quote! { false }, - }; - let attribute = |name: &Ident| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { - let as_string = name.to_string(); - quote!(#as_string) - } - }; - - let attribute = match attr { - ElementAttr::AttrText { name, value } => { - let ns = ns(name); - let volitile = volitile(name); - let attribute = attribute(name); - quote! { - __cx.attr( - #attribute, - #value, - #ns, - #volitile - ) - } - } - ElementAttr::AttrExpression { name, value } => { - let ns = ns(name); - let volitile = volitile(name); - let attribute = attribute(name); - quote! { - __cx.attr( - #attribute, - #value, - #ns, - #volitile - ) - } - } - ElementAttr::CustomAttrText { name, value } => { - quote! { - __cx.attr( - #name, - #value, - None, - false - ) - } - } - ElementAttr::CustomAttrExpression { name, value } => { - quote! { - __cx.attr( - #name, - #value, - None, - false - ) - } - } - ElementAttr::EventTokens { name, tokens } => { - quote! { - dioxus_elements::events::#name(__cx, #tokens) - } - } - }; - - tokens.append_all(attribute); - } -} - -// ::dioxus::core::Attribute { -// name: stringify!(#name), -// namespace: None, -// volatile: false, -// mounted_node: Default::default(), -// value: ::dioxus::core::AttributeValue::Text(#value), -// } diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs index f89b28500..6cc34e1fb 100644 --- a/packages/rsx/src/errors.rs +++ b/packages/rsx/src/errors.rs @@ -1,12 +1,12 @@ macro_rules! missing_trailing_comma { ($span:expr) => { - return Err(Error::new($span, "missing trailing comma")); + return Err(syn::Error::new($span, "missing trailing comma")); }; } macro_rules! attr_after_element { ($span:expr) => { - return Err(Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); + return Err(syn::Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); }; } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index f52ecad0e..ff00fd87e 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -13,6 +13,7 @@ #[macro_use] mod errors; +mod attribute; mod component; mod element; #[cfg(feature = "hot_reload")] @@ -23,6 +24,7 @@ mod node; use std::{fmt::Debug, hash::Hash}; // Re-export the namespaces into each other +pub use attribute::*; pub use component::*; #[cfg(feature = "hot_reload")] use dioxus_core::{Template, TemplateAttribute, TemplateNode}; @@ -307,16 +309,9 @@ impl DynamicMapping { match node { BodyNode::Element(el) => { for attr in el.attributes { - match &attr.attr { - ElementAttr::CustomAttrText { value, .. } - | ElementAttr::AttrText { value, .. } - if value.is_static() => {} - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + match &attr.attr.value { + ElementAttrValue::AttrLiteral(input) if input.is_static() => {} + _ => { self.insert_attribute(attr.attr); } } @@ -365,9 +360,10 @@ impl<'a> DynamicContext<'a> { let mut static_attrs = Vec::new(); for attr in &el.attributes { - match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { + match &attr.attr.value { + ElementAttrValue::AttrLiteral(value) if value.is_static() => { let value = value.source.as_ref().unwrap(); + let name = &attr.attr.name; let attribute_name_rust = name.to_string(); let (name, namespace) = Ctx::map_attribute(&element_name_rust, &attribute_name_rust) @@ -379,20 +375,7 @@ impl<'a> DynamicContext<'a> { }) } - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); - static_attrs.push(TemplateAttribute::Static { - name: intern(name.value().as_str()), - namespace: None, - value: intern(value.value().as_str()), - }) - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + _ => { let idx = match mapping { Some(mapping) => mapping.get_attribute_idx(&attr.attr)?, None => self.dynamic_attributes.len(), @@ -464,10 +447,16 @@ impl<'a> DynamicContext<'a> { ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, ElementName::Custom(_) => quote! { None }, }; - let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { + let static_attrs = el.attributes.iter().map(|attr| match &attr.attr.value { + ElementAttrValue::AttrLiteral(value) if value.is_static() => { let value = value.to_static().unwrap(); - let ns = ns(quote!(#name.1)); + let ns = { + match &attr.attr.name { + ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), + ElementAttrName::Custom(_) => quote!(None), + } + }; + let name = &attr.attr.name; let name = match el_name { ElementName::Ident(_) => quote! { #el_name::#name.0 }, ElementName::Custom(_) => { @@ -487,25 +476,7 @@ impl<'a> DynamicContext<'a> { } } - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.to_static().unwrap(); - quote! { - ::dioxus::core::TemplateAttribute::Static { - name: #name, - namespace: None, - value: #value, - - // todo: we don't diff these so we never apply the volatile flag - // volatile: dioxus_elements::#el_name::#name.2, - } - } - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + _ => { let ct = self.dynamic_attributes.len(); self.dynamic_attributes.push(attr); self.attr_paths.push(self.current_path.clone()); From 30b124189cabb04e7f723296e6436c20822a0024 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 14 Sep 2023 15:56:17 -0500 Subject: [PATCH 02/11] fix raw attribute names --- packages/rsx/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index ff00fd87e..ea24a779f 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -457,9 +457,11 @@ impl<'a> DynamicContext<'a> { } }; let name = &attr.attr.name; - let name = match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { + let name = match (el_name, name) { + (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { + quote! { #el_name::#name.0 } + } + _ => { let as_string = name.to_string(); quote! { #as_string } } From be4b0c801a9df54abc32d34dafdf3f9f51560338 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 14 Sep 2023 16:10:48 -0500 Subject: [PATCH 03/11] fix raw attributes with dynamic values --- packages/rsx/src/attribute.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 76cf55a01..ea9d79263 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -4,12 +4,7 @@ use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - spanned::Spanned, - Expr, Ident, LitStr, Result, Token, -}; +use syn::{Expr, Ident, LitStr}; #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct ElementAttrNamed { @@ -38,13 +33,17 @@ impl ToTokens for ElementAttrNamed { fn to_tokens(&self, tokens: &mut TokenStream2) { let ElementAttrNamed { el_name, attr } = self; - let ns = |name| match el_name { - ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 }, - ElementName::Custom(_) => quote! { None }, + let ns = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.1 } + } + _ => quote! { None }, }; - let volitile = |name| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.2 }, - ElementName::Custom(_) => quote! { false }, + let volitile = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.2 } + } + _ => quote! { false }, }; let attribute = |name: &ElementAttrName| match name { ElementAttrName::BuiltIn(name) => match el_name { From 1a17ca988c209d36c82c336449fa195559601362 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 14 Sep 2023 16:19:21 -0500 Subject: [PATCH 04/11] fix double quotes in autoformatting --- packages/autofmt/src/element.rs | 2 +- packages/autofmt/tests/samples/simple.rsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 971f4447e..b64415649 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -215,7 +215,7 @@ impl Writer<'_> { write!(self.out, "{}", name)?; } ElementAttrName::Custom(name) => { - write!(self.out, "\"{}\"", name.to_token_stream())?; + write!(self.out, "{}", name.to_token_stream())?; } } diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx index a0e573974..a3bfd1388 100644 --- a/packages/autofmt/tests/samples/simple.rsx +++ b/packages/autofmt/tests/samples/simple.rsx @@ -33,7 +33,7 @@ rsx! { } // No children, minimal props - img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" } + img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png" } // One level compression div { From 9f1c735cf1d939ee82a2aa7e65f17d8e9b0dd1d9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 14 Sep 2023 16:40:49 -0500 Subject: [PATCH 05/11] combine style and class attributes when there are multiple --- packages/rsx/src/attribute.rs | 46 +++++++++++++++++++++++++++++++---- packages/rsx/src/element.rs | 16 +++++++++++- packages/rsx/src/ifmt.rs | 21 +++++++++++++++- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index ea9d79263..a5daafc27 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -13,14 +13,14 @@ pub struct ElementAttrNamed { } impl ElementAttrNamed { - pub(crate) fn try_combine(&self, other: Self) -> Option { + pub(crate) fn try_combine(&self, other: &Self) -> Option { if self.el_name == other.el_name && self.attr.name == other.attr.name { - if let Some(separator) = todo!() { + if let Some(separator) = self.attr.name.multi_attribute_separator() { return Some(ElementAttrNamed { el_name: self.el_name.clone(), attr: ElementAttr { name: self.attr.name.clone(), - value: self.attr.value.combine(separator, other.attr.value), + value: self.attr.value.combine(separator, &other.attr.value), }, }); } @@ -109,8 +109,33 @@ pub enum ElementAttrValue { } impl ElementAttrValue { - fn combine(&self, separator: &str, other: Self) -> Self { - todo!() + fn combine(&self, separator: &str, other: &Self) -> Self { + match (self, other) { + (Self::AttrLiteral(lit1), Self::AttrLiteral(lit2)) => { + let fmt = lit1.clone().join(lit2.clone(), separator); + Self::AttrLiteral(fmt) + } + (Self::AttrLiteral(expr1), Self::AttrExpr(expr2)) => { + let mut ifmt = expr1.clone(); + ifmt.push_str(separator); + ifmt.push_expr(expr2.clone()); + Self::AttrLiteral(ifmt) + } + (Self::AttrExpr(expr1), Self::AttrLiteral(expr2)) => { + let mut ifmt = expr2.clone(); + ifmt.push_str(separator); + ifmt.push_expr(expr1.clone()); + Self::AttrLiteral(ifmt) + } + (Self::AttrExpr(expr1), Self::AttrExpr(expr2)) => { + let mut ifmt = IfmtInput::default(); + ifmt.push_expr(expr1.clone()); + ifmt.push_str(separator); + ifmt.push_expr(expr2.clone()); + Self::AttrLiteral(ifmt) + } + _ => todo!(), + } } } @@ -121,6 +146,17 @@ pub enum ElementAttrName { } impl ElementAttrName { + fn multi_attribute_separator(&self) -> Option<&'static str> { + match self { + ElementAttrName::BuiltIn(i) => match i.to_string().as_str() { + "class" => Some(" "), + "style" => Some(";"), + _ => None, + }, + ElementAttrName::Custom(_) => None, + } + } + pub fn start(&self) -> Span { match self { ElementAttrName::BuiltIn(i) => i.span(), diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 19557edd2..a02669fc7 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -1,4 +1,7 @@ -use std::fmt::{Display, Formatter}; +use std::{ + collections::HashMap, + fmt::{Display, Formatter}, +}; use super::*; @@ -134,6 +137,17 @@ impl Parse for Element { // Deduplicate any attributes that can be combined // For example, if there are two `class` attributes, combine them into one + let mut combined_attrs: HashMap = HashMap::new(); + for attr in attributes { + if let Some(old_attr) = combined_attrs.get_mut(&attr.attr.name) { + if let Some(combined) = old_attr.try_combine(&attr) { + *old_attr = combined; + } + } else { + combined_attrs.insert(attr.attr.name.clone(), attr); + } + } + let attributes: Vec<_> = combined_attrs.into_iter().map(|(_, v)| v).collect(); while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index 6c72f5025..981b111ce 100644 --- a/packages/rsx/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -13,7 +13,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result { } #[allow(dead_code)] // dumb compiler does not see the struct being used... -#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] pub struct IfmtInput { pub source: Option, pub segments: Vec, @@ -27,6 +27,25 @@ impl IfmtInput { } } + pub fn join(mut self, other: Self, separator: &str) -> Self { + if !self.segments.is_empty() { + self.segments.push(Segment::Literal(separator.to_string())); + } + self.segments.extend(other.segments); + self + } + + pub fn push_expr(&mut self, expr: Expr) { + self.segments.push(Segment::Formatted(FormattedSegment { + format_args: String::new(), + segment: FormattedSegmentType::Expr(Box::new(expr)), + })); + } + + pub fn push_str(&mut self, s: &str) { + self.segments.push(Segment::Literal(s.to_string())); + } + pub fn is_static(&self) -> bool { matches!(self.segments.as_slice(), &[Segment::Literal(_)] | &[]) } From d297e2baa293ea680c03466d2df52cd711eefc4e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 Sep 2023 19:50:02 -0500 Subject: [PATCH 06/11] Add optional attributes --- packages/autofmt/src/element.rs | 9 +++ packages/autofmt/src/writer.rs | 58 +++++++++------- packages/core/src/nodes.rs | 6 ++ packages/rsx/src/attribute.rs | 118 ++++++++++++++++++++++++++++++-- packages/rsx/src/element.rs | 35 +++------- packages/rsx/src/node.rs | 2 +- 6 files changed, 171 insertions(+), 57 deletions(-) diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index b64415649..c583dc593 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -224,6 +224,15 @@ impl Writer<'_> { fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result { match value { + ElementAttrValue::AttrOptionalExpr { condition, value } => { + write!( + self.out, + "if {condition} {{ ", + condition = prettyplease::unparse_expr(condition), + )?; + self.write_attribute_value(value)?; + write!(self.out, " }}")?; + } ElementAttrValue::AttrLiteral(value) => { write!(self.out, "{value}", value = ifmt_to_string(value))?; } diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index c96e96c93..788007319 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -132,6 +132,39 @@ impl<'a> Writer<'a> { Ok(()) } + pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize { + match value { + ElementAttrValue::AttrOptionalExpr { condition, value } => { + let condition_len = self.retrieve_formatted_expr(condition).len(); + let value_len = self.attr_value_len(value); + + condition_len + value_len + 6 + } + ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), + ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), + ElementAttrValue::EventTokens(tokens) => { + let location = Location::new(tokens.span().start()); + + let len = if let std::collections::hash_map::Entry::Vacant(e) = + self.cached_formats.entry(location) + { + let formatted = prettyplease::unparse_expr(tokens); + let len = if formatted.contains('\n') { + 10000 + } else { + formatted.len() + }; + e.insert(formatted); + len + } else { + self.cached_formats[&location].len() + }; + + len + } + } + } + pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { let mut total = 0; @@ -154,30 +187,7 @@ impl<'a> Writer<'a> { dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, }; - total += match &attr.attr.value { - ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), - ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), - ElementAttrValue::EventTokens(tokens) => { - let location = Location::new(tokens.span().start()); - - let len = if let std::collections::hash_map::Entry::Vacant(e) = - self.cached_formats.entry(location) - { - let formatted = prettyplease::unparse_expr(tokens); - let len = if formatted.contains('\n') { - 10000 - } else { - formatted.len() - }; - e.insert(formatted); - len - } else { - self.cached_formats[&location].len() - }; - - len - } - }; + total += self.attr_value_len(&attr.attr.value); total += 6; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c83b1059a..bccbc154b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -791,6 +791,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str { } } +impl<'a> IntoAttributeValue<'a> for String { + fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Text(bump.alloc(self)) + } +} + impl<'a> IntoAttributeValue<'a> for f64 { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Float(self) diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index a5daafc27..c3bb11516 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -4,7 +4,7 @@ use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{Expr, Ident, LitStr}; +use syn::{parse_quote, Expr, ExprIf, Ident, LitStr}; #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct ElementAttrNamed { @@ -58,16 +58,15 @@ impl ToTokens for ElementAttrNamed { let attribute = { match &attr.value { - ElementAttrValue::AttrLiteral(_) | ElementAttrValue::AttrExpr(_) => { + ElementAttrValue::AttrLiteral(_) + | ElementAttrValue::AttrExpr(_) + | ElementAttrValue::AttrOptionalExpr { .. } => { let name = &self.attr.name; let ns = ns(name); let volitile = volitile(name); let attribute = attribute(name); - let value = match &self.attr.value { - ElementAttrValue::AttrLiteral(lit) => quote! { #lit }, - ElementAttrValue::AttrExpr(expr) => quote! { #expr }, - _ => unreachable!(), - }; + let value = &self.attr.value; + let value = quote! { #value }; quote! { __cx.attr( #attribute, @@ -102,13 +101,62 @@ pub struct ElementAttr { pub enum ElementAttrValue { /// attribute: "value" AttrLiteral(IfmtInput), + /// attribute: if bool { "value" } + AttrOptionalExpr { + condition: Expr, + value: Box, + }, /// attribute: true AttrExpr(Expr), /// onclick: move |_| {} EventTokens(Expr), } +impl Parse for ElementAttrValue { + fn parse(input: ParseStream) -> syn::Result { + Ok(if input.peek(Token![if]) { + let if_expr = input.parse::()?; + if is_if_chain_terminated(&if_expr) { + ElementAttrValue::AttrExpr(Expr::If(if_expr)) + } else { + ElementAttrValue::AttrOptionalExpr { + condition: *if_expr.cond, + value: Box::new(syn::parse2(if_expr.then_branch.into_token_stream())?), + } + } + } else if input.peek(LitStr) { + let value = input.parse()?; + ElementAttrValue::AttrLiteral(value) + } else { + let value = input.parse::()?; + ElementAttrValue::AttrExpr(value) + }) + } +} + +impl ToTokens for ElementAttrValue { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementAttrValue::AttrLiteral(lit) => tokens.append_all(quote! { #lit }), + ElementAttrValue::AttrOptionalExpr { condition, value } => { + tokens.append_all(quote! { if #condition { Some(#value) } else { None } }) + } + ElementAttrValue::AttrExpr(expr) => tokens.append_all(quote! { #expr }), + ElementAttrValue::EventTokens(expr) => tokens.append_all(quote! { #expr }), + } + } +} + impl ElementAttrValue { + fn to_str_expr(&self) -> Option { + match self { + ElementAttrValue::AttrLiteral(lit) => Some(quote!(#lit.to_string())), + ElementAttrValue::AttrOptionalExpr { value, .. } => value.to_str_expr(), + ElementAttrValue::AttrExpr(expr) => Some(quote!(#expr.to_string())), + _ => None, + } + } + fn combine(&self, separator: &str, other: &Self) -> Self { match (self, other) { (Self::AttrLiteral(lit1), Self::AttrLiteral(lit2)) => { @@ -134,6 +182,62 @@ impl ElementAttrValue { ifmt.push_expr(expr2.clone()); Self::AttrLiteral(ifmt) } + ( + Self::AttrOptionalExpr { + condition: condition1, + value: value1, + }, + Self::AttrOptionalExpr { + condition: condition2, + value: value2, + }, + ) => { + let first_as_string = value1.to_str_expr(); + let second_as_string = value2.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = String::new(); + if #condition1 { + __combined.push_str(&#first_as_string); + } + if #condition2 { + if __combined.len() > 0 { + __combined.push_str(&#separator); + } + __combined.push_str(&#second_as_string); + } + __combined + } + }) + } + (Self::AttrOptionalExpr { condition, value }, other) => { + let first_as_string = value.to_str_expr(); + let second_as_string = other.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = #second_as_string; + if #condition { + __combined.push_str(&#separator); + __combined.push_str(&#first_as_string); + } + __combined + } + }) + } + (other, Self::AttrOptionalExpr { condition, value }) => { + let first_as_string = other.to_str_expr(); + let second_as_string = value.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = #first_as_string; + if #condition { + __combined.push_str(&#separator); + __combined.push_str(&#second_as_string); + } + __combined + } + }) + } _ => todo!(), } } diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index a02669fc7..609205145 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -11,7 +11,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, spanned::Spanned, - Expr, Ident, LitStr, Result, Token, + Ident, LitStr, Result, Token, }; // ======================================= @@ -51,13 +51,7 @@ impl Parse for Element { content.parse::()?; - let value = if content.peek(LitStr) { - let value = content.parse()?; - ElementAttrValue::AttrLiteral(value) - } else { - let value = content.parse::()?; - ElementAttrValue::AttrExpr(value) - }; + let value = content.parse::()?; attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { @@ -100,23 +94,14 @@ impl Parse for Element { key = Some(content.parse()?); } _ => { - if content.peek(LitStr) { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr { - name: ElementAttrName::BuiltIn(name), - value: ElementAttrValue::AttrLiteral(content.parse()?), - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr { - name: ElementAttrName::BuiltIn(name), - value: ElementAttrValue::AttrExpr(content.parse()?), - }, - }); - } + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value, + }, + }); } } } diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 1bda081d9..e1a8aab0f 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -247,7 +247,7 @@ impl Parse for ForLoop { } } -fn is_if_chain_terminated(chain: &ExprIf) -> bool { +pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool { let mut current = chain; loop { if let Some((_, else_block)) = ¤t.else_branch { From ab2b9fb34e7cd69c6511462bfa7be4ac56229ffb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 09:39:53 -0500 Subject: [PATCH 07/11] fix autoformatting --- packages/autofmt/src/element.rs | 1 + packages/core/tests/kitchen_sink.rs | 3 +- packages/rsx-rosetta/src/lib.rs | 1 + packages/rsx/src/element.rs | 26 +++++----- packages/rsx/src/lib.rs | 75 +++++++++++++++-------------- 5 files changed, 57 insertions(+), 49 deletions(-) diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index c583dc593..1e741740c 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,6 +49,7 @@ impl Writer<'_> { attributes, children, brace, + .. } = el; /* diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 191d1da7c..4cb5f87fb 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -24,6 +24,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { } }) } + #[test] fn dual_stream() { let mut dom = VirtualDom::new(basic_syntax_is_a_template); @@ -36,7 +37,7 @@ fn dual_stream() { LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", - value: (&*bump.alloc("123".into_value(&bump))).into(), + value: (&*bump.alloc("ast 123".into_value(&bump))).into(), id: ElementId(1), ns: None, }, diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 19b1d5375..865bf15e8 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -79,6 +79,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { name: el_name, children, attributes, + merged_attributes: Default::default(), key: None, brace: Default::default(), })) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 609205145..86d530d33 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashMap, - fmt::{Display, Formatter}, -}; +use std::fmt::{Display, Formatter}; use super::*; @@ -22,6 +19,7 @@ pub struct Element { pub name: ElementName, pub key: Option, pub attributes: Vec, + pub merged_attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, } @@ -122,17 +120,20 @@ impl Parse for Element { // Deduplicate any attributes that can be combined // For example, if there are two `class` attributes, combine them into one - let mut combined_attrs: HashMap = HashMap::new(); - for attr in attributes { - if let Some(old_attr) = combined_attrs.get_mut(&attr.attr.name) { - if let Some(combined) = old_attr.try_combine(&attr) { + let mut merged_attributes: Vec = Vec::new(); + for attr in &attributes { + if let Some(old_attr_index) = merged_attributes + .iter() + .position(|a| a.attr.name == attr.attr.name) + { + let old_attr = &mut merged_attributes[old_attr_index]; + if let Some(combined) = old_attr.try_combine(attr) { *old_attr = combined; } } else { - combined_attrs.insert(attr.attr.name.clone(), attr); + merged_attributes.push(attr.clone()); } } - let attributes: Vec<_> = combined_attrs.into_iter().map(|(_, v)| v).collect(); while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { @@ -155,6 +156,7 @@ impl Parse for Element { key, name: el_name, attributes, + merged_attributes, children, brace, }) @@ -172,12 +174,12 @@ impl ToTokens for Element { }; let listeners = self - .attributes + .merged_attributes .iter() .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); let attr = self - .attributes + .merged_attributes .iter() .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index ea24a779f..356379d09 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -308,7 +308,7 @@ impl DynamicMapping { fn add_node(&mut self, node: BodyNode) { match node { BodyNode::Element(el) => { - for attr in el.attributes { + for attr in el.merged_attributes { match &attr.attr.value { ElementAttrValue::AttrLiteral(input) if input.is_static() => {} _ => { @@ -359,7 +359,7 @@ impl<'a> DynamicContext<'a> { let element_name_rust = el.name.to_string(); let mut static_attrs = Vec::new(); - for attr in &el.attributes { + for attr in &el.merged_attributes { match &attr.attr.value { ElementAttrValue::AttrLiteral(value) if value.is_static() => { let value = value.source.as_ref().unwrap(); @@ -447,44 +447,47 @@ impl<'a> DynamicContext<'a> { ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, ElementName::Custom(_) => quote! { None }, }; - let static_attrs = el.attributes.iter().map(|attr| match &attr.attr.value { - ElementAttrValue::AttrLiteral(value) if value.is_static() => { - let value = value.to_static().unwrap(); - let ns = { - match &attr.attr.name { - ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), - ElementAttrName::Custom(_) => quote!(None), - } - }; - let name = &attr.attr.name; - let name = match (el_name, name) { - (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { - quote! { #el_name::#name.0 } - } - _ => { - let as_string = name.to_string(); - quote! { #as_string } - } - }; - quote! { - ::dioxus::core::TemplateAttribute::Static { - name: #name, - namespace: #ns, - value: #value, + let static_attrs = el + .merged_attributes + .iter() + .map(|attr| match &attr.attr.value { + ElementAttrValue::AttrLiteral(value) if value.is_static() => { + let value = value.to_static().unwrap(); + let ns = { + match &attr.attr.name { + ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), + ElementAttrName::Custom(_) => quote!(None), + } + }; + let name = &attr.attr.name; + let name = match (el_name, name) { + (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { + quote! { #el_name::#name.0 } + } + _ => { + let as_string = name.to_string(); + quote! { #as_string } + } + }; + quote! { + ::dioxus::core::TemplateAttribute::Static { + name: #name, + namespace: #ns, + value: #value, - // todo: we don't diff these so we never apply the volatile flag - // volatile: dioxus_elements::#el_name::#name.2, + // todo: we don't diff these so we never apply the volatile flag + // volatile: dioxus_elements::#el_name::#name.2, + } } } - } - _ => { - let ct = self.dynamic_attributes.len(); - self.dynamic_attributes.push(attr); - self.attr_paths.push(self.current_path.clone()); - quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } - } - }); + _ => { + let ct = self.dynamic_attributes.len(); + self.dynamic_attributes.push(attr); + self.attr_paths.push(self.current_path.clone()); + quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } + } + }); let attrs = quote! { #(#static_attrs),*}; From c0f345e775c24bb253cc5b7524640d6877cee31b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 09:48:56 -0500 Subject: [PATCH 08/11] add if statements to test and rsx usage example --- examples/rsx_usage.rs | 5 +++++ packages/core/tests/kitchen_sink.rs | 4 +++- packages/rsx/src/attribute.rs | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index d5e2fcdb3..96326316d 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -53,6 +53,7 @@ fn app(cx: Scope) -> Element { let formatting = "formatting!"; let formatting_tuple = ("a", "b"); let lazy_fmt = format_args!("lazily formatted text"); + let asd = 123; cx.render(rsx! { div { // Elements @@ -80,6 +81,10 @@ fn app(cx: Scope) -> Element { // pass simple rust expressions in class: lazy_fmt, id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), + class: "asd", + class: "{asd}", + // if statements can be used to conditionally render attributes + class: if formatting.contains("form") { "{asd}" }, div { class: { const WORD: &str = "expressions"; diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 4cb5f87fb..0abe3756e 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -10,6 +10,8 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { div { key: "12345", class: "asd", class: "{asd}", + class: if true { "{asd}" }, + class: if false { "{asd}" }, onclick: move |_| {}, div { "{var}" } div { @@ -37,7 +39,7 @@ fn dual_stream() { LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", - value: (&*bump.alloc("ast 123".into_value(&bump))).into(), + value: (&*bump.alloc("asd 123 123".into_value(&bump))).into(), id: ElementId(1), ns: None, }, diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index c3bb11516..38cb7c853 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -121,7 +121,12 @@ impl Parse for ElementAttrValue { } else { ElementAttrValue::AttrOptionalExpr { condition: *if_expr.cond, - value: Box::new(syn::parse2(if_expr.then_branch.into_token_stream())?), + value: { + let stmts = if_expr.then_branch.stmts; + Box::new(syn::parse2(quote! { + #(#stmts)* + })?) + }, } } } else if input.peek(LitStr) { From 9be7773ba72373594303a70fc247d84fbb4af21a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 09:50:30 -0500 Subject: [PATCH 09/11] add to tailwind example --- examples/tailwind/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs index 2dbb183c9..8a49071db 100644 --- a/examples/tailwind/src/main.rs +++ b/examples/tailwind/src/main.rs @@ -14,9 +14,13 @@ fn main() { } pub fn app(cx: Scope) -> Element { + let grey_background = true; cx.render(rsx!( div { - header { class: "text-gray-400 bg-gray-900 body-font", + header { + class: "text-gray-400 body-font", + // you can use optional attributes to optionally apply a tailwind class + class: if grey_background { "bg-gray-900" }, div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", StacksIcon {} From 561830d81451363473c013c2bb3802425f607d28 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 10:20:42 -0500 Subject: [PATCH 10/11] fix merging static attributes --- packages/rsx/src/ifmt.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index 981b111ce..fa764bf2e 100644 --- a/packages/rsx/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -47,7 +47,9 @@ impl IfmtInput { } pub fn is_static(&self) -> bool { - matches!(self.segments.as_slice(), &[Segment::Literal(_)] | &[]) + self.segments + .iter() + .all(|seg| matches!(seg, Segment::Literal(_))) } } From 610401b928e9eb3844bf732ce24644076272f27c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 10:23:36 -0500 Subject: [PATCH 11/11] fix repeated class in ssr test --- packages/ssr/src/renderer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 154fad641..54dce45e5 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -224,7 +224,7 @@ fn to_string_works() { assert_eq!( item.1.segments, vec![ - PreRendered("
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); + assert_eq!(out, "
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); }