From 7e9d5264049d5a5645e35bd1c5323288bfb7d02f Mon Sep 17 00:00:00 2001 From: Alexandre Macabies Date: Thu, 28 Oct 2021 21:27:26 +0200 Subject: [PATCH] Add support for Option attributes Introduces the `attr=[value]` syntax that assumes `value` is an `Option`. Renders `attr="value"` for `Some(value)` and entirely omits the attribute for `None`. Implements and therefore closes #283. --- CHANGELOG.md | 2 ++ docs/content/elements-attributes.md | 20 +++++++++++++++++ maud/tests/basic_syntax.rs | 34 +++++++++++++++++++++++++++++ maud_macros/src/ast.rs | 2 ++ maud_macros/src/generate.rs | 19 ++++++++++++++++ maud_macros/src/parse.rs | 31 +++++++++++++++++++++----- 6 files changed, 103 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 073ba41c..b8667e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Update to support axum 0.2 [#303](https://github.com/lambda-fairy/maud/pull/303) +- Add support for `Option` attributes using the `attr=[value]` syntax. + [#306](https://github.com/lambda-fairy/maud/pull/306) ## [0.22.3] - 2021-09-27 diff --git a/docs/content/elements-attributes.md b/docs/content/elements-attributes.md index 868cb250..561e851d 100644 --- a/docs/content/elements-attributes.md +++ b/docs/content/elements-attributes.md @@ -101,6 +101,26 @@ html! { # ; ``` +## Optional attributes: `title=[Some("value")]` + +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 +# let _ = maud:: +html! { + p title=[Some("Good password")] { "Correct horse" } + + @let value = Some(42); + input value=[value]; + + @let title: Option<&str> = None; + p title=[title] { "Battery staple" } +} +# ; +``` + ## Empty attributes: `checked` Declare an empty attribute by omitting the value. diff --git a/maud/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs index e7f73ca1..64e682d2 100644 --- a/maud/tests/basic_syntax.rs +++ b/maud/tests/basic_syntax.rs @@ -118,6 +118,40 @@ fn empty_attributes_question_mark() { assert_eq!(result.into_string(), ""); } +#[test] +fn optional_attribute_some() { + let result = html! { input value=[Some("value")]; }; + assert_eq!(result.into_string(), r#""#); +} + +#[test] +fn optional_attribute_none() { + let result = html! { input value=[None as Option<&str>]; }; + assert_eq!(result.into_string(), r#""#); +} + +#[test] +fn optional_attribute_non_string_some() { + let result = html! { input value=[Some(42)]; }; + assert_eq!(result.into_string(), r#""#); +} + +#[test] +fn optional_attribute_variable() { + let result = html! { + @let x = Some(42); + input value=[x]; + }; + assert_eq!(result.into_string(), r#""#); +} + +#[test] +fn optional_attribute_inner_value_evaluated_only_once() { + let mut count = 0; + html! { input value=[{ count += 1; Some("picklebarrelkumquat") }]; }; + assert_eq!(count, 1); +} + #[test] fn colons_in_names() { let result = html! { pon-pon:controls-alpha { a on:click="yay()" { "Yay!" } } }; diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index fdf35d68..9c363930 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -170,6 +170,7 @@ impl Attribute { #[derive(Debug)] pub enum AttrType { Normal { value: Markup }, + Optional { cond: TokenStream, value: Markup }, Empty { toggler: Option }, } @@ -177,6 +178,7 @@ impl AttrType { fn span(&self) -> Option { match *self { AttrType::Normal { ref value } => Some(value.span()), + AttrType::Optional { ref value, .. } => Some(value.span()), AttrType::Empty { ref toggler } => toggler.as_ref().map(Toggler::span), } } diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index 9fa60fef..ad1768b6 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -124,6 +124,25 @@ impl Generator { self.markup(value, build); build.push_str("\""); } + AttrType::Optional { cond, value } => { + build.push_tokens({ + let some_value = match value { + Markup::Splice { outer_span, .. } => Markup::Splice { + expr: quote!(inner_value), + outer_span, + }, + _ => unreachable!(), + }; + let mut build = self.builder(); + build.push_str(" "); + self.name(name, &mut build); + build.push_str("=\""); + self.markup(some_value, &mut build); + build.push_str("\""); + let body = build.finish(); + quote!(if let Some(inner_value) = #cond { #body }) + }) + } AttrType::Empty { toggler: None } => { build.push_str(" "); self.name(name, build); diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index bf78f2dd..873ee573 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -571,13 +571,16 @@ impl Parser { // Parse a value under an attribute context assert!(self.current_attr.is_none()); self.current_attr = Some(ast::name_to_string(name.clone())); - let value = self.markup(); + let attr_type = match self.optional_attr() { + Some(optional_attr) => optional_attr, + None => { + let value = self.markup(); + ast::AttrType::Normal { value } + } + }; self.current_attr = None; attrs.push(ast::Attr::Attribute { - attribute: ast::Attribute { - name, - attr_type: ast::AttrType::Normal { value }, - }, + attribute: ast::Attribute { name, attr_type }, }); } // Empty attribute (legacy syntax) @@ -688,6 +691,24 @@ impl Parser { } } + // Parses the `[optional value]` syntax after an attribute declaration. + fn optional_attr(&mut self) -> Option { + match self.peek() { + Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => { + self.advance(); + let value = ast::Markup::Splice { + expr: group.stream(), + outer_span: SpanRange::single_span(group.span()), + }; + Some(ast::AttrType::Optional { + cond: group.stream(), + value, + }) + } + _ => None, + } + } + /// Parses an identifier, without dealing with namespaces. fn try_name(&mut self) -> Option { let mut result = Vec::new();