From 15236ea5792e30543b280b8de5a66e5772d8a4b2 Mon Sep 17 00:00:00 2001 From: Alexandre Macabies Date: Tue, 19 Oct 2021 23:28:46 +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. --- docs/content/elements-attributes.md | 20 ++++++++++++++++++++ maud/tests/basic_syntax.rs | 20 ++++++++++++++++++++ maud_macros/src/ast.rs | 2 ++ maud_macros/src/generate.rs | 20 ++++++++++++++++++++ maud_macros/src/parse.rs | 28 ++++++++++++++++++++++++++-- 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/docs/content/elements-attributes.md b/docs/content/elements-attributes.md index 868cb250..e94a5dc3 100644 --- a/docs/content/elements-attributes.md +++ b/docs/content/elements-attributes.md @@ -101,6 +101,26 @@ html! { # ; ``` +## Optional attributes: `title=[Some("value")]` + +Add attributes to an element using the `attr=[value]` syntax (note the square +brackets) that are only rendered if the value is `Some`, and entirely omitted +if the value is `None`. + +```rust +# let _ = maud:: +html! { + p class=[Some("hidden")] { "Correct horse" } + + @let value = Some(42); + input value=[value]; + + @let class: Option<&str> = None; + p class=[class] { "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..ac39755a 100644 --- a/maud/tests/basic_syntax.rs +++ b/maud/tests/basic_syntax.rs @@ -118,6 +118,26 @@ fn empty_attributes_question_mark() { assert_eq!(result.into_string(), ""); } +#[test] +fn optional_attributes() { + let result = html! { input value=[Some("value")]; }; + assert_eq!(result.into_string(), r#""#); + let result = html! { input value=[Some(42)]; }; + assert_eq!(result.into_string(), r#""#); + let result = html! { input value=[None as Option<&str>]; }; + assert_eq!(result.into_string(), r#""#); + let result = html! { + @let x = Some(42); + input value=[x]; + }; + assert_eq!(result.into_string(), r#""#); + let result = html! { + @let x: Option = None; + input value=[x]; + }; + assert_eq!(result.into_string(), r#""#); +} + #[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..742adb21 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -124,6 +124,26 @@ impl Generator { self.markup(value, build); build.push_str("\""); } + AttrType::Optional { cond, value } => { + build.push_tokens({ + let unwrapped = match value { + Markup::Splice { expr, outer_span } => Markup::Splice { + // Safe, because this is behind .is_some(). + expr: quote!((#expr).unwrap()), + outer_span, + }, + _ => unreachable!(), + }; + let mut build = self.builder(); + build.push_str(" "); + self.name(name, &mut build); + build.push_str("=\""); + self.markup(unwrapped, &mut build); + build.push_str("\""); + let body = build.finish(); + quote!(if (#cond).is_some() { #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..315020f4 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -571,12 +571,18 @@ 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 }, + attr_type, }, }); } @@ -688,6 +694,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();