diff --git a/examples/spread.rs b/examples/spread.rs index bab1c426f..75c026aff 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -73,7 +73,7 @@ impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> { impl ExtendedGlobalAttributesMarker for Props<'_> {} fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { - let attributes = &cx.props.attributes; + let attributes = &*cx.props.attributes; render! { audio { ..attributes, diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 4f47331c1..ba4f51451 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,7 +49,6 @@ impl Writer<'_> { attributes, children, brace, - extra_attributes, .. } = el; @@ -167,7 +166,7 @@ impl Writer<'_> { fn write_attributes( &mut self, - attributes: &[ElementAttrNamed], + attributes: &[AttributeType], key: &Option, sameline: bool, ) -> Result { @@ -189,7 +188,7 @@ impl Writer<'_> { while let Some(attr) = attr_iter.next() { self.out.indent += 1; if !sameline { - self.write_comments(attr.attr.start())?; + self.write_comments(attr.start())?; } self.out.indent -= 1; @@ -290,7 +289,14 @@ impl Writer<'_> { Ok(()) } - fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { + fn write_attribute(&mut self, attr: &AttributeType) -> Result { + match attr { + AttributeType::Named(attr) => self.write_named_attribute(attr), + AttributeType::Spread(attr) => self.write_spread_attribute(attr), + } + } + + fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result { self.write_attribute_name(&attr.attr.name)?; write!(self.out, ": ")?; self.write_attribute_value(&attr.attr.value)?; @@ -298,6 +304,13 @@ impl Writer<'_> { Ok(()) } + fn write_spread_attribute(&mut self, attr: &Expr) -> Result { + write!(self.out, "..")?; + write!(self.out, "{}", prettyplease::unparse_expr(attr))?; + + 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 788007319..165267714 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,4 +1,4 @@ -use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop}; +use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; use std::{ @@ -165,12 +165,12 @@ impl<'a> Writer<'a> { } } - pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { + pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize { let mut total = 0; for attr in attributes { - if self.current_span_is_primary(attr.attr.start()) { - 'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() { + if self.current_span_is_primary(attr.start()) { + 'line: for line in self.src[..attr.start().start().line - 1].iter().rev() { match (line.trim().starts_with("//"), line.is_empty()) { (true, _) => return 100000, (_, true) => continue 'line, @@ -179,16 +179,24 @@ impl<'a> Writer<'a> { } } - total += match &attr.attr.name { - dioxus_rsx::ElementAttrName::BuiltIn(name) => { - let name = name.to_string(); - name.len() + match attr { + AttributeType::Named(attr) => { + let name_len = match &attr.attr.name { + dioxus_rsx::ElementAttrName::BuiltIn(name) => { + let name = name.to_string(); + name.len() + } + dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, + }; + total += name_len; + total += self.attr_value_len(&attr.attr.value); + } + AttributeType::Spread(expr) => { + let expr_len = self.retrieve_formatted_expr(expr).len(); + total += expr_len + 3; } - dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, }; - total += self.attr_value_len(&attr.attr.value); - total += 6; } diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 22c1c52a9..2497e4411 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -1,6 +1,7 @@ use convert_case::{Case, Casing}; use dioxus_rsx::{ - BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput, + AttributeType, BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, + ElementName, IfmtInput, }; pub use html_parser::{Dom, Node}; use proc_macro2::{Ident, Span}; @@ -34,7 +35,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { Ident::new(new_name.as_str(), Span::call_site()) }; - ElementAttrNamed { + AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text( @@ -42,13 +43,13 @@ pub fn rsx_node_from_html(node: &Node) -> Option { )), name: dioxus_rsx::ElementAttrName::BuiltIn(ident), }, - } + }) }) .collect(); let class = el.classes.join(" "); if !class.is_empty() { - attributes.push(ElementAttrNamed { + attributes.push(AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( @@ -57,11 +58,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option { )), value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)), }, - }); + })); } if let Some(id) = &el.id { - attributes.push(ElementAttrNamed { + attributes.push(AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( @@ -70,7 +71,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { )), value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)), }, - }); + })); } let children = el.children.iter().filter_map(rsx_node_from_html).collect(); @@ -82,7 +83,6 @@ pub fn rsx_node_from_html(node: &Node) -> Option { merged_attributes: Default::default(), key: None, brace: Default::default(), - extra_attributes: None, })) } diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 38cb7c853..6f5d0a096 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -4,7 +4,38 @@ use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{parse_quote, Expr, ExprIf, Ident, LitStr}; +use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum AttributeType { + Named(ElementAttrNamed), + Spread(Expr), +} + +impl AttributeType { + pub fn start(&self) -> Span { + match self { + AttributeType::Named(n) => n.attr.start(), + AttributeType::Spread(e) => e.span(), + } + } + + pub(crate) fn try_combine(&self, other: &Self) -> Option { + match (self, other) { + (Self::Named(a), Self::Named(b)) => a.try_combine(b).map(Self::Named), + _ => None, + } + } +} + +impl ToTokens for AttributeType { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + AttributeType::Named(n) => tokens.append_all(quote! { #n }), + AttributeType::Spread(e) => tokens.append_all(quote! { #e.into() }), + } + } +} #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct ElementAttrNamed { diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 66ee422c2..e62fc6d6e 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, - Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -18,11 +18,10 @@ use syn::{ pub struct Element { pub name: ElementName, pub key: Option, - pub attributes: Vec, - pub merged_attributes: Vec, + pub attributes: Vec, + pub merged_attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, - pub extra_attributes: Option, } impl Parse for Element { @@ -33,7 +32,7 @@ impl Parse for Element { let content: ParseBuffer; let brace = syn::braced!(content in stream); - let mut attributes: Vec = vec![]; + let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; @@ -43,9 +42,20 @@ impl Parse for Element { // "def": 456, // abc: 123, loop { - if content.peek(Token![...]) { - content.parse::()?; - extra_attributes = Some(content.parse::()?); + if content.peek(Token![..]) { + content.parse::()?; + let expr = content.parse::()?; + let span = expr.span(); + attributes.push(attribute::AttributeType::Spread(expr)); + + if content.is_empty() { + break; + } + + if content.parse::().is_err() { + missing_trailing_comma!(span); + } + continue; } // Parse the raw literal fields @@ -56,13 +66,13 @@ impl Parse for Element { content.parse::()?; let value = content.parse::()?; - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: ElementAttrName::Custom(name), value, }, - }); + })); if content.is_empty() { break; @@ -85,13 +95,13 @@ impl Parse for Element { let span = content.span(); if name_str.starts_with("on") { - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: ElementAttrName::BuiltIn(name), value: ElementAttrValue::EventTokens(content.parse()?), }, - }); + })); } else { match name_str.as_str() { "key" => { @@ -99,13 +109,13 @@ impl Parse for Element { } _ => { let value = content.parse::()?; - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: ElementAttrName::BuiltIn(name), value, }, - }); + })); } } } @@ -114,7 +124,6 @@ impl Parse for Element { break; } - // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { missing_trailing_comma!(span); } @@ -126,12 +135,26 @@ 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 merged_attributes: Vec = Vec::new(); + 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) - { + if let Some(old_attr_index) = merged_attributes.iter().position(|a| { + matches!((a, attr), ( + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + name: ElementAttrName::BuiltIn(old_name), + .. + }, + .. + }), + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + name: ElementAttrName::BuiltIn(new_name), + .. + }, + .. + }), + ) if old_name == new_name) + }) { let old_attr = &mut merged_attributes[old_attr_index]; if let Some(combined) = old_attr.try_combine(attr) { *old_attr = combined; @@ -165,7 +188,6 @@ impl Parse for Element { merged_attributes, children, brace, - extra_attributes, }) } } @@ -180,15 +202,31 @@ impl ToTokens for Element { None => quote! { None }, }; - let listeners = self - .merged_attributes - .iter() - .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); + let listeners = self.merged_attributes.iter().filter(|f| { + matches!( + f, + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + value: ElementAttrValue::EventTokens { .. }, + .. + }, + .. + }) + ) + }); - let attr = self - .merged_attributes - .iter() - .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); + let attr = self.merged_attributes.iter().filter(|f| { + !matches!( + f, + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + value: ElementAttrValue::EventTokens { .. }, + .. + }, + .. + }) + ) + }); tokens.append_all(quote! { __cx.element( diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 356379d09..24bb8b533 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -261,7 +261,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { #[cfg(feature = "hot_reload")] #[derive(Default, Debug)] struct DynamicMapping { - attribute_to_idx: std::collections::HashMap>, + attribute_to_idx: std::collections::HashMap>, last_attribute_idx: usize, node_to_idx: std::collections::HashMap>, last_element_idx: usize, @@ -277,7 +277,7 @@ impl DynamicMapping { new } - fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option { + fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option { self.attribute_to_idx .get_mut(attr) .and_then(|idxs| idxs.pop()) @@ -287,7 +287,7 @@ impl DynamicMapping { self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop()) } - fn insert_attribute(&mut self, attr: ElementAttr) -> usize { + fn insert_attribute(&mut self, attr: AttributeType) -> usize { let idx = self.last_attribute_idx; self.last_attribute_idx += 1; @@ -309,10 +309,17 @@ impl DynamicMapping { match node { BodyNode::Element(el) => { for attr in el.merged_attributes { - match &attr.attr.value { - ElementAttrValue::AttrLiteral(input) if input.is_static() => {} + match &attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(input), + .. + }, + .. + }) if input.is_static() => {} _ => { - self.insert_attribute(attr.attr); + self.insert_attribute(attr); } } } @@ -340,7 +347,7 @@ impl DynamicMapping { #[derive(Default, Debug)] pub struct DynamicContext<'a> { dynamic_nodes: Vec<&'a BodyNode>, - dynamic_attributes: Vec<&'a ElementAttrNamed>, + dynamic_attributes: Vec<&'a AttributeType>, current_path: Vec, node_paths: Vec>, @@ -360,10 +367,16 @@ impl<'a> DynamicContext<'a> { let mut static_attrs = Vec::new(); for attr in &el.merged_attributes { - match &attr.attr.value { - ElementAttrValue::AttrLiteral(value) if value.is_static() => { + match &attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(value), + name, + }, + .. + }) 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) @@ -377,7 +390,7 @@ impl<'a> DynamicContext<'a> { _ => { let idx = match mapping { - Some(mapping) => mapping.get_attribute_idx(&attr.attr)?, + Some(mapping) => mapping.get_attribute_idx(&attr)?, None => self.dynamic_attributes.len(), }; self.dynamic_attributes.push(attr); @@ -447,47 +460,50 @@ impl<'a> DynamicContext<'a> { ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, ElementName::Custom(_) => quote! { None }, }; - 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, + let static_attrs = el.merged_attributes.iter().map(|attr| match attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(value), + name, + }, + .. + }) if value.is_static() => { + let value = value.to_static().unwrap(); + let ns = { + match name { + ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), + ElementAttrName::Custom(_) => quote!(None), + } + }; + 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),*};