Merge pull request #1463 from ealmloff/many_optional_attributes

Implement multiple optional attributes
This commit is contained in:
Jonathan Kelley 2024-01-04 10:19:04 -08:00 committed by GitHub
commit d933291c90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 548 additions and 323 deletions

View file

@ -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";

View file

@ -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 {}

View file

@ -49,6 +49,7 @@ impl Writer<'_> {
attributes,
children,
brace,
..
} = el;
/*
@ -209,12 +210,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();
@ -222,9 +245,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 +260,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 +269,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 +289,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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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,
},

View file

@ -59,6 +59,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
ElementAttrNamed {
el_name: el_name.clone(),
// attr: ElementAttr {
// value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(
// value.as_deref().unwrap_or("false"),
// )),
// name: dioxus_rsx::ElementAttrName::BuiltIn(ident),
// },
attr,
}
})
@ -68,9 +74,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
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)),
},
});
}
@ -78,9 +87,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
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)),
},
});
}
@ -91,6 +103,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
name: el_name,
children,
attributes,
merged_attributes: Default::default(),
key: None,
brace: Default::default(),
}))

View file

@ -23,5 +23,7 @@ krates = { version = "0.12.6", optional = true }
tracing.workspace = true
[features]
default = ["html"]
hot_reload = ["krates", "internment"]
serde = ["dep:serde"]
html = []

View file

@ -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<Self> {
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<ElementAttrValue>,
},
/// attribute: true
AttrExpr(Expr),
/// onclick: move |_| {}
EventTokens(Expr),
}
impl Parse for ElementAttrValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(if input.peek(Token![if]) {
let if_expr = input.parse::<ExprIf>()?;
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::<Expr>()?;
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<TokenStream2> {
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(_),
..
}
)
}
}

View file

@ -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<IfmtInput>,
pub attributes: Vec<ElementAttrNamed>,
pub merged_attributes: Vec<ElementAttrNamed>,
pub children: Vec<BodyNode>,
pub brace: syn::token::Brace,
}
@ -34,7 +35,6 @@ impl Parse for Element {
let mut attributes: Vec<ElementAttrNamed> = vec![];
let mut children: Vec<BodyNode> = 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 +49,14 @@ impl Parse for Element {
content.parse::<Token![:]>()?;
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::<Expr>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrExpression { name, value },
});
}
let value = content.parse::<ElementAttrValue>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: ElementAttrName::Custom(name),
value,
},
});
if content.is_empty() {
break;
@ -86,9 +81,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,29 +91,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::<Expr>()?);
}
_ => {
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::<ElementAttrValue>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: ElementAttrName::BuiltIn(name),
value,
},
});
}
}
}
@ -137,6 +118,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<ElementAttrNamed> = 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());
@ -158,6 +156,7 @@ impl Parse for Element {
key,
name: el_name,
attributes,
merged_attributes,
children,
brace,
})
@ -175,14 +174,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(
@ -264,136 +263,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),
// }

View file

@ -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"));
};
}

View file

@ -13,7 +13,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
}
#[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<LitStr>,
pub segments: Vec<Segment>,
@ -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(_)))
}
}

View file

@ -16,6 +16,7 @@
#[macro_use]
mod errors;
mod attribute;
mod component;
mod element;
#[cfg(feature = "hot_reload")]
@ -26,6 +27,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};
@ -313,17 +315,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);
}
}
@ -371,10 +366,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)
@ -386,20 +382,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(),
@ -471,54 +454,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),*};

View file

@ -260,7 +260,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)) = &current.else_branch {

View file

@ -213,7 +213,7 @@ fn to_string_works() {
assert_eq!(
item.1.segments,
vec![
PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
PreRendered("<div class=\"asdasdasd asdasdasd\"".into(),),
Attr(0,),
StyleMarker {
inside_style_tag: false,
@ -235,7 +235,7 @@ fn to_string_works() {
use Segment::*;
assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
assert_eq!(out, "<div class=\"asdasdasd asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
}
#[test]