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..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..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();