diff --git a/packages/rsx_interpreter/src/captuered_context.rs b/packages/rsx_interpreter/src/captuered_context.rs index 8473771e9..b3ee7636d 100644 --- a/packages/rsx_interpreter/src/captuered_context.rs +++ b/packages/rsx_interpreter/src/captuered_context.rs @@ -1,11 +1,13 @@ +use std::collections::HashSet; + use dioxus_core::{Listener, VNode}; use dioxus_rsx::{ BodyNode, CallBody, Component, ElementAttr, ElementAttrNamed, IfmtInput, Segment, }; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{Expr, Ident, Result}; +use syn::{Expr, Ident, LitStr, Result}; -use crate::CodeLocation; +use crate::{attributes::attrbute_to_static_str, CodeLocation}; #[derive(Default)] pub struct CapturedContextBuilder { pub ifmted: Vec, @@ -14,6 +16,7 @@ pub struct CapturedContextBuilder { pub captured_expressions: Vec, pub listeners: Vec, pub custom_context: Option, + pub custom_attributes: HashSet, } impl CapturedContextBuilder { @@ -23,6 +26,7 @@ impl CapturedContextBuilder { self.iterators.extend(other.iterators); self.listeners.extend(other.listeners); self.captured_expressions.extend(other.captured_expressions); + self.custom_attributes.extend(other.custom_attributes); } pub fn from_call_body(body: CallBody) -> Result { @@ -42,8 +46,13 @@ impl CapturedContextBuilder { BodyNode::Element(el) => { for attr in el.attributes { match attr.attr { - ElementAttr::AttrText { value, .. } - | ElementAttr::CustomAttrText { value, .. } => { + ElementAttr::AttrText { value, .. } => { + let value_tokens = value.to_token_stream(); + let formated: IfmtInput = syn::parse2(value_tokens)?; + captured.ifmted.push(formated); + } + ElementAttr::CustomAttrText { value, name } => { + captured.custom_attributes.insert(name); let value_tokens = value.to_token_stream(); let formated: IfmtInput = syn::parse2(value_tokens)?; captured.ifmted.push(formated); @@ -51,7 +60,8 @@ impl CapturedContextBuilder { ElementAttr::AttrExpression { name: _, value } => { captured.captured_expressions.push(value); } - ElementAttr::CustomAttrExpression { name: _, value } => { + ElementAttr::CustomAttrExpression { name, value } => { + captured.custom_attributes.insert(name); captured.captured_expressions.push(value); } ElementAttr::EventTokens { .. } => captured.listeners.push(attr), @@ -91,6 +101,7 @@ impl ToTokens for CapturedContextBuilder { captured_expressions, listeners, custom_context: _, + custom_attributes, } = self; let listeners_str = listeners .iter() @@ -113,9 +124,9 @@ impl ToTokens for CapturedContextBuilder { let expr = segment.to_token_stream(); let as_string = expr.to_string(); let format_expr = if format_args.is_empty() { - "{".to_string() + format_args + "}" + "{".to_string() + &format_args + "}" } else { - "{".to_string() + ":" + format_args + "}" + "{".to_string() + ":" + &format_args + "}" }; Some(quote! { FormattedArg{ @@ -131,6 +142,7 @@ impl ToTokens for CapturedContextBuilder { let captured_attr_expressions_text = captured_expressions .iter() .map(|e| format!("{}", e.to_token_stream())); + let custom_attributes_iter = custom_attributes.iter(); tokens.append_all(quote! { CapturedContext { captured: IfmtArgs{ @@ -140,6 +152,7 @@ impl ToTokens for CapturedContextBuilder { iterators: vec![#((#iterators_str, #iterators)),*], expressions: vec![#((#captured_attr_expressions_text, #captured_expressions.to_string())),*], listeners: vec![#((#listeners_str, #listeners)),*], + custom_attributes: &[#(#custom_attributes_iter),*], location: code_location.clone() } }) @@ -159,10 +172,31 @@ pub struct CapturedContext<'a> { pub expressions: Vec<(&'static str, String)>, // map listener code to the resulting listener pub listeners: Vec<(&'static str, Listener<'a>)>, + // used to map custom attrbutes form &'a str to &'static str + pub custom_attributes: &'static [&'static str], // used to provide better error messages pub location: CodeLocation, } +impl<'a> CapturedContext<'a> { + pub fn attrbute_to_static_str( + &self, + attr: &str, + literal: bool, + ) -> Option<(&'static str, Option<&'static str>)> { + if let Some(attr) = attrbute_to_static_str(attr) { + Some(attr) + } else if literal { + self.custom_attributes + .iter() + .find(|attribute| attr == **attribute) + .map(|attribute| (*attribute, None)) + } else { + None + } + } +} + pub struct IfmtArgs { // All expressions that have been resolved pub named_args: Vec, diff --git a/packages/rsx_interpreter/src/error.rs b/packages/rsx_interpreter/src/error.rs index 368d81d6f..599365769 100644 --- a/packages/rsx_interpreter/src/error.rs +++ b/packages/rsx_interpreter/src/error.rs @@ -15,6 +15,7 @@ pub enum RecompileReason { CapturedExpression(String), CapturedComponent(String), CapturedListener(String), + CapturedAttribute(String), } #[derive(Debug, Serialize, Deserialize)] diff --git a/packages/rsx_interpreter/src/interperter.rs b/packages/rsx_interpreter/src/interperter.rs index f35488c05..73dd1ebdb 100644 --- a/packages/rsx_interpreter/src/interperter.rs +++ b/packages/rsx_interpreter/src/interperter.rs @@ -5,7 +5,6 @@ use quote::__private::Span; use std::str::FromStr; use syn::{parse2, parse_str, Expr}; -use crate::attributes::attrbute_to_static_str; use crate::captuered_context::{CapturedContext, IfmtArgs}; use crate::elements::element_to_static_str; use crate::error::{Error, ParseError, RecompileReason}; @@ -39,7 +38,7 @@ fn resolve_ifmt(ifmt: &IfmtInput, captured: &IfmtArgs) -> Result } } } - Segment::Literal(lit) => result.push_str(lit), + Segment::Literal(lit) => result.push_str(&lit), } } Ok(result) @@ -80,25 +79,35 @@ fn build_node<'a>( for attr in &el.attributes { match &attr.attr { ElementAttr::AttrText { .. } | ElementAttr::CustomAttrText { .. } => { - let (name, value, span): (String, IfmtInput, Span) = match &attr.attr { - ElementAttr::AttrText { name, value } => ( - name.to_string(), - IfmtInput::from_str(&value.value()).map_err(|err| { - Error::ParseError(ParseError::new(err, ctx.location.clone())) - })?, - name.span(), - ), - ElementAttr::CustomAttrText { name, value } => ( - name.value(), - IfmtInput::from_str(&value.value()).map_err(|err| { - Error::ParseError(ParseError::new(err, ctx.location.clone())) - })?, - name.span(), - ), - _ => unreachable!(), - }; + let (name, value, span, literal): (String, IfmtInput, Span, bool) = + match &attr.attr { + ElementAttr::AttrText { name, value } => ( + name.to_string(), + IfmtInput::from_str(&value.value()).map_err(|err| { + Error::ParseError(ParseError::new( + err, + ctx.location.clone(), + )) + })?, + name.span(), + false, + ), + ElementAttr::CustomAttrText { name, value } => ( + name.value(), + IfmtInput::from_str(&value.value()).map_err(|err| { + Error::ParseError(ParseError::new( + err, + ctx.location.clone(), + )) + })?, + name.span(), + true, + ), + _ => unreachable!(), + }; - if let Some((name, namespace)) = attrbute_to_static_str(&name) { + if let Some((name, namespace)) = ctx.attrbute_to_static_str(&name, literal) + { let value = bump.alloc(resolve_ifmt(&value, &ctx.captured)?); attributes.push(Attribute { name, @@ -108,21 +117,28 @@ fn build_node<'a>( namespace, }); } else { - return Err(Error::ParseError(ParseError::new( - syn::Error::new(span, format!("unknown attribute: {}", name)), - ctx.location.clone(), - ))); + if literal { + // literals will be captured when a full recompile is triggered + return Err(Error::RecompileRequiredError( + RecompileReason::CapturedAttribute(name.to_string()), + )); + } else { + return Err(Error::ParseError(ParseError::new( + syn::Error::new(span, format!("unknown attribute: {}", name)), + ctx.location.clone(), + ))); + } } } ElementAttr::AttrExpression { .. } | ElementAttr::CustomAttrExpression { .. } => { - let (name, value) = match &attr.attr { + let (name, value, span, literal) = match &attr.attr { ElementAttr::AttrExpression { name, value } => { - (name.to_string(), value) + (name.to_string(), value, name.span(), false) } ElementAttr::CustomAttrExpression { name, value } => { - (name.value(), value) + (name.value(), value, name.span(), true) } _ => unreachable!(), }; @@ -131,7 +147,9 @@ fn build_node<'a>( .iter() .find(|(n, _)| parse_str::(*n).unwrap() == *value) { - if let Some((name, namespace)) = attrbute_to_static_str(&name) { + if let Some((name, namespace)) = + ctx.attrbute_to_static_str(&name, literal) + { let value = bump.alloc(resulting_value.clone()); attributes.push(Attribute { name, @@ -140,6 +158,21 @@ fn build_node<'a>( is_volatile: false, namespace, }); + } else { + if literal { + // literals will be captured when a full recompile is triggered + return Err(Error::RecompileRequiredError( + RecompileReason::CapturedAttribute(name.to_string()), + )); + } else { + return Err(Error::ParseError(ParseError::new( + syn::Error::new( + span, + format!("unknown attribute: {}", name), + ), + ctx.location.clone(), + ))); + } } } else { return Err(Error::RecompileRequiredError( diff --git a/packages/rsx_interpreter/tests/render.rs b/packages/rsx_interpreter/tests/render.rs index 661d16c5d..7553ec179 100644 --- a/packages/rsx_interpreter/tests/render.rs +++ b/packages/rsx_interpreter/tests/render.rs @@ -24,6 +24,7 @@ fn render_basic() { expressions: Vec::new(), listeners: Vec::new(), location: location.clone(), + custom_attributes: &[], }; let interperted_vnodes = LazyNodes::new(|factory| { dioxus_rsx_interpreter::resolve_scope( @@ -70,6 +71,7 @@ fn render_nested() { expressions: Vec::new(), listeners: Vec::new(), location: location.clone(), + custom_attributes: &[], }; let interperted_vnodes = LazyNodes::new(|factory| { dioxus_rsx_interpreter::resolve_scope( @@ -90,6 +92,54 @@ fn render_nested() { assert!(check_eq(interperted_vnodes, static_vnodes)); } +#[test] +#[allow(non_snake_case)] +fn render_custom_attribute() { + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + let dom = VirtualDom::new(Base); + let static_vnodes = rsx! { + div { + "data-test-1": 0, + "data-test-2": "1", + } + }; + let location = CodeLocation { + file_path: String::new(), + crate_path: String::new(), + line: 2, + column: 0, + }; + let empty_context = CapturedContext { + captured: IfmtArgs { + named_args: Vec::new(), + }, + components: Vec::new(), + iterators: Vec::new(), + expressions: vec![("0", "0".to_string())], + listeners: Vec::new(), + location: location.clone(), + custom_attributes: &["data-test-1", "data-test-2"], + }; + let interperted_vnodes = LazyNodes::new(|factory| { + dioxus_rsx_interpreter::resolve_scope( + location, + r#"div { + "data-test-1": 0, + "data-test-2": "1", + }"#, + empty_context, + factory, + ) + }); + + let interperted_vnodes = dom.render_vnodes(interperted_vnodes); + let static_vnodes = dom.render_vnodes(static_vnodes); + assert!(check_eq(interperted_vnodes, static_vnodes)); +} + #[test] #[allow(non_snake_case)] fn render_component() { @@ -110,7 +160,7 @@ fn render_component() { let location = CodeLocation { file_path: String::new(), crate_path: String::new(), - line: 2, + line: 3, column: 0, }; @@ -127,6 +177,7 @@ fn render_component() { expressions: Vec::new(), listeners: Vec::new(), location: location.clone(), + custom_attributes: &[], }; dioxus_rsx_interpreter::resolve_scope( location, @@ -162,7 +213,7 @@ fn render_iterator() { let location = CodeLocation { file_path: String::new(), crate_path: String::new(), - line: 3, + line: 4, column: 0, }; @@ -180,6 +231,7 @@ fn render_iterator() { expressions: Vec::new(), listeners: Vec::new(), location: location.clone(), + custom_attributes: &[], }; dioxus_rsx_interpreter::resolve_scope( location, @@ -216,7 +268,7 @@ fn render_captured_variable() { let location = CodeLocation { file_path: String::new(), crate_path: String::new(), - line: 4, + line: 5, column: 0, }; @@ -234,6 +286,7 @@ fn render_captured_variable() { expressions: Vec::new(), listeners: Vec::new(), location: location.clone(), + custom_attributes: &[], }; dioxus_rsx_interpreter::resolve_scope( location, @@ -268,7 +321,7 @@ fn render_listener() { let location = CodeLocation { file_path: String::new(), crate_path: String::new(), - line: 5, + line: 6, column: 0, }; @@ -287,6 +340,7 @@ fn render_listener() { dioxus_elements::on::onclick(factory, f), )], location: location.clone(), + custom_attributes: &[], }; dioxus_rsx_interpreter::resolve_scope( location,