From 794e60e0e32f028ff7abaf15239d60f16fb266d2 Mon Sep 17 00:00:00 2001 From: Bad Manners Date: Sat, 28 Sep 2024 11:00:42 -0300 Subject: [PATCH] Allow splices in attribute names Closes #444 --- docs/content/splices-toggles.md | 16 ++++- maud/tests/splices.rs | 10 ++++ maud_macros/src/ast.rs | 29 ++++++++- maud_macros/src/generate.rs | 19 ++++-- maud_macros/src/parse.rs | 103 ++++++++++++++++++-------------- 5 files changed, 125 insertions(+), 52 deletions(-) diff --git a/docs/content/splices-toggles.md b/docs/content/splices-toggles.md index 84f8bf3c..87d55fd3 100644 --- a/docs/content/splices-toggles.md +++ b/docs/content/splices-toggles.md @@ -90,6 +90,20 @@ html! { # ; ``` +### Splices in attribute name + +You can also use splices in the attribute name: + +```rust +let tuple = ("hx-get", "/pony"); +# let _ = maud:: +html! { + button (tuple.0)=(tuple.1) { + "Get a pony!" + } +} +``` + ### What can be spliced? You can splice any value that implements [`Render`][Render]. @@ -145,7 +159,7 @@ html! { ### Optional attributes with values: `title=[Some("value")]` -Add optional attributes to an element using `attr=[value]` syntax, with *square* brackets. +Add optional attributes to an element using `attr=[value]` syntax, with _square_ brackets. These are only rendered if the value is `Some`, and entirely omitted if the value is `None`. ```rust diff --git a/maud/tests/splices.rs b/maud/tests/splices.rs index 8665e84b..7e2b6d0c 100644 --- a/maud/tests/splices.rs +++ b/maud/tests/splices.rs @@ -73,6 +73,16 @@ fn locals() { assert_eq!(result.into_string(), "Pinkie Pie"); } +#[test] +fn attribute_name() { + let tuple = ("hx-get", "/pony"); + let result = html! { button (tuple.0)=(tuple.1) { "Get a pony!" } }; + assert_eq!( + result.into_string(), + r#""# + ); +} + /// An example struct, for testing purposes only struct Creature { name: &'static str, diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index b95665ec..61a5c166 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -153,13 +153,13 @@ impl Special { #[derive(Debug)] pub struct NamedAttr { - pub name: TokenStream, + pub name: AttrName, pub attr_type: AttrType, } impl NamedAttr { fn span(&self) -> SpanRange { - let name_span = span_tokens(self.name.clone()); + let name_span = span_tokens(self.name.tokens()); if let Some(attr_type_span) = self.attr_type.span() { name_span.join_range(attr_type_span) } else { @@ -168,6 +168,31 @@ impl NamedAttr { } } +#[derive(Debug, Clone)] +pub enum AttrName { + Fixed { value: TokenStream }, + Splice { expr: TokenStream }, +} + +impl AttrName { + pub fn tokens(&self) -> TokenStream { + match self { + AttrName::Fixed { value } => value.clone(), + AttrName::Splice { expr, .. } => expr.clone(), + } + } +} + +impl std::fmt::Display for AttrName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AttrName::Fixed { value } => f.write_str(&value.to_string())?, + AttrName::Splice { expr, .. } => f.write_str(&expr.to_string())?, + }; + Ok(()) + } +} + #[derive(Debug)] pub enum AttrType { Normal { value: Markup }, diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index ee27a55d..f432a500 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -123,12 +123,19 @@ impl Generator { build.push_escaped(&name_to_string(name)); } + fn attr_name(&self, name: AttrName, build: &mut Builder) { + match name { + AttrName::Fixed { value } => self.name(value, build), + AttrName::Splice { expr, .. } => self.splice(expr, build), + } + } + fn attrs(&self, attrs: Vec, build: &mut Builder) { for NamedAttr { name, attr_type } in desugar_attrs(attrs) { match attr_type { AttrType::Normal { value } => { build.push_str(" "); - self.name(name, build); + self.attr_name(name, build); build.push_str("=\""); self.markup(value, build); build.push_str("\""); @@ -140,7 +147,7 @@ impl Generator { let body = { let mut build = self.builder(); build.push_str(" "); - self.name(name, &mut build); + self.attr_name(name, &mut build); build.push_str("=\""); self.splice(inner_value.clone(), &mut build); build.push_str("\""); @@ -150,7 +157,7 @@ impl Generator { } AttrType::Empty { toggler: None } => { build.push_str(" "); - self.name(name, build); + self.attr_name(name, build); } AttrType::Empty { toggler: Some(Toggler { cond, .. }), @@ -158,7 +165,7 @@ impl Generator { let body = { let mut build = self.builder(); build.push_str(" "); - self.name(name, &mut build); + self.attr_name(name, &mut build); build.finish() }; build.push_tokens(quote!(if (#cond) { #body })); @@ -224,7 +231,9 @@ fn desugar_classes_or_ids( }); } Some(NamedAttr { - name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))), + name: AttrName::Fixed { + value: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))), + }, attr_type: AttrType::Normal { value: Markup::Block(Block { markups, diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 05af2894..0376e0e9 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -13,7 +13,7 @@ pub fn parse(input: TokenStream) -> Vec { #[derive(Clone)] struct Parser { /// If we're inside an attribute, then this contains the attribute name. - current_attr: Option, + current_attr: Option, input: ::IntoIter, } @@ -580,48 +580,7 @@ impl Parser { let mut attrs = Vec::new(); loop { if let Some(name) = self.try_namespaced_name() { - // Attribute - match self.peek() { - // Non-empty attribute - Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => { - self.advance(); - // Parse a value under an attribute context - assert!(self.current_attr.is_none()); - self.current_attr = Some(ast::name_to_string(name.clone())); - let attr_type = match self.attr_toggler() { - Some(toggler) => ast::AttrType::Optional { toggler }, - None => { - let value = self.markup(); - ast::AttrType::Normal { value } - } - }; - self.current_attr = None; - attrs.push(ast::Attr::Named { - named_attr: ast::NamedAttr { name, attr_type }, - }); - } - // Empty attribute (legacy syntax) - Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => { - self.advance(); - let toggler = self.attr_toggler(); - attrs.push(ast::Attr::Named { - named_attr: ast::NamedAttr { - name: name.clone(), - attr_type: ast::AttrType::Empty { toggler }, - }, - }); - } - // Empty attribute (new syntax) - _ => { - let toggler = self.attr_toggler(); - attrs.push(ast::Attr::Named { - named_attr: ast::NamedAttr { - name: name.clone(), - attr_type: ast::AttrType::Empty { toggler }, - }, - }); - } - } + attrs.push(self.attr(ast::AttrName::Fixed { value: name })); } else { match self.peek() { // Class shorthand @@ -644,6 +603,18 @@ impl Parser { name, }); } + // Spliced attribute name + Some(TokenTree::Group(ref group)) + if group.delimiter() == Delimiter::Parenthesis => + { + match self.markup() { + ast::Markup::Splice { expr, .. } => { + attrs.push(self.attr(ast::AttrName::Splice { expr })); + } + // If it's not a splice, backtrack and bail out + _ => break, + } + } // If it's not a valid attribute, backtrack and bail out _ => break, } @@ -665,7 +636,7 @@ impl Parser { ast::Attr::Id { .. } => "id".to_string(), ast::Attr::Named { named_attr } => named_attr .name - .clone() + .tokens() .into_iter() .map(|token| token.to_string()) .collect(), @@ -685,6 +656,50 @@ impl Parser { attrs } + fn attr(&mut self, name: ast::AttrName) -> ast::Attr { + match self.peek() { + // Non-empty attribute + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => { + self.advance(); + // Parse a value under an attribute context + assert!(self.current_attr.is_none()); + self.current_attr = Some(name.clone()); + let attr_type = match self.attr_toggler() { + Some(toggler) => ast::AttrType::Optional { toggler }, + None => { + let value = self.markup(); + ast::AttrType::Normal { value } + } + }; + self.current_attr = None; + ast::Attr::Named { + named_attr: ast::NamedAttr { name, attr_type }, + } + } + // Empty attribute (legacy syntax) + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => { + self.advance(); + let toggler = self.attr_toggler(); + ast::Attr::Named { + named_attr: ast::NamedAttr { + name: name, + attr_type: ast::AttrType::Empty { toggler }, + }, + } + } + // Empty attribute (new syntax) + _ => { + let toggler = self.attr_toggler(); + ast::Attr::Named { + named_attr: ast::NamedAttr { + name: name, + attr_type: ast::AttrType::Empty { toggler }, + }, + } + } + } + } + /// Parses the name of a class or ID. fn class_or_id_name(&mut self) -> ast::Markup { if let Some(symbol) = self.try_name() {