diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index f9ecb4939..317288d55 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/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 {} diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 44f527337..4f47331c1 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -50,6 +50,7 @@ impl Writer<'_> { children, brace, extra_attributes, + .. } = el; /* @@ -210,12 +211,34 @@ 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::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))?; + } + ElementAttrValue::AttrExpr(value) => { let out = prettyplease::unparse_expr(value); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); @@ -223,9 +246,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()?; @@ -238,22 +261,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(); @@ -262,9 +270,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()?; @@ -282,6 +290,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..788007319 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::{ @@ -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; @@ -146,40 +179,17 @@ impl<'a> Writer<'a> { } } - total += match &attr.attr { - ElementAttr::AttrText { value, name } => { - ifmt_to_string(value).len() + name.span().line_length() + 6 - } - 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 } => { - 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 + name.span().line_length() + 6 + total += 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 += self.attr_value_len(&attr.attr.value); + + total += 6; } total @@ -218,7 +228,7 @@ impl<'a> Writer<'a> { } } -trait SpanLength { +pub(crate) trait SpanLength { fn line_length(&self) -> usize; } impl SpanLength for Span { 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 { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 963246951..a31481195 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -851,6 +851,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/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 191d1da7c..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 { @@ -24,6 +26,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 +39,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("asd 123 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 97678cf3a..22c1c52a9 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)), }, }); } @@ -71,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(), extra_attributes: None, 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..38cb7c853 --- /dev/null +++ b/packages/rsx/src/attribute.rs @@ -0,0 +1,309 @@ +use std::fmt::{Display, Formatter}; + +use super::*; + +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse_quote, Expr, ExprIf, Ident, LitStr}; + +#[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) = 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), + }, + }); + } + } + None + } +} + +impl ToTokens for ElementAttrNamed { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ElementAttrNamed { el_name, attr } = self; + + let ns = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.1 } + } + _ => quote! { None }, + }; + 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 { + 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(_) + | ElementAttrValue::AttrOptionalExpr { .. } => { + let name = &self.attr.name; + let ns = ns(name); + let volitile = volitile(name); + let attribute = attribute(name); + let value = &self.attr.value; + let value = quote! { #value }; + 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: 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: { + let stmts = if_expr.then_branch.stmts; + Box::new(syn::parse2(quote! { + #(#stmts)* + })?) + }, + } + } + } 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)) => { + 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) + } + ( + 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!(), + } + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrName { + BuiltIn(Ident), + Custom(LitStr), +} + +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(), + 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 f64ab9809..66ee422c2 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, + Ident, LitStr, Result, Token, }; // ======================================= @@ -19,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, pub extra_attributes: Option, @@ -35,8 +36,6 @@ impl Parse for Element { let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; - let mut _el_ref = None; - let mut extra_attributes = None; // parse fields with commas // break when we don't get this pattern anymore @@ -56,19 +55,14 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) { - let value = content.parse()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); - } else { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); - } + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::Custom(name), + value, + }, + }); if content.is_empty() { break; @@ -93,9 +87,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 { @@ -103,29 +97,15 @@ 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()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrExpression { - name, - value: content.parse()?, - }, - }); - } + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value, + }, + }); } } } @@ -144,6 +124,23 @@ impl Parse for Element { break; } + // 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(); + 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 { + merged_attributes.push(attr.clone()); + } + } + while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { attr_after_element!(content.span()); @@ -165,6 +162,7 @@ impl Parse for Element { key, name: el_name, attributes, + merged_attributes, children, brace, extra_attributes, @@ -183,14 +181,14 @@ impl ToTokens for Element { }; let listeners = self - .attributes + .merged_attributes .iter() - .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); let attr = self - .attributes + .merged_attributes .iter() - .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); tokens.append_all(quote! { __cx.element( @@ -272,128 +270,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); - } -} 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/ifmt.rs b/packages/rsx/src/ifmt.rs index 6c72f5025..fa764bf2e 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,8 +27,29 @@ 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(_)] | &[]) + self.segments + .iter() + .all(|seg| matches!(seg, Segment::Literal(_))) } } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index f52ecad0e..356379d09 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}; @@ -306,17 +308,10 @@ impl DynamicMapping { fn add_node(&mut self, node: BodyNode) { 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 { .. } => { + for attr in el.merged_attributes { + match &attr.attr.value { + ElementAttrValue::AttrLiteral(input) if input.is_static() => {} + _ => { self.insert_attribute(attr.attr); } } @@ -364,10 +359,11 @@ impl<'a> DynamicContext<'a> { let element_name_rust = el.name.to_string(); let mut static_attrs = Vec::new(); - for attr in &el.attributes { - match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { + for attr in &el.merged_attributes { + 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,54 +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 { - ElementAttr::AttrText { name, value } if value.is_static() => { - let value = value.to_static().unwrap(); - let ns = ns(quote!(#name.1)); - let name = match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { - 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, + } } } - } - 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, - } + _ => { + 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 } } } - } - - 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()); - quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } - } - }); + }); let attrs = quote! { #(#static_attrs),*}; 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 { diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 589b399b1..d22610eac 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -216,7 +216,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
"); } pub(crate) const BOOL_ATTRS: &[&str] = &[