Skip to content

Commit

Permalink
Add support for Option<T> attributes
Browse files Browse the repository at this point in the history
Introduces the `attr=[value]` syntax that assumes `value` is an
`Option<T>`. Renders `attr="value"` for `Some(value)` and entirely
omits the attribute for `None`.

Implements and therefore closes #283.
  • Loading branch information
zopieux committed Oct 28, 2021
1 parent 057a231 commit b787c18
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- Update to support axum 0.2
[#303](https://github.com/lambda-fairy/maud/pull/303)
- Add support for `Option<T>` attributes using the `attr=[value]` syntax.
[#306](https://github.com/lambda-fairy/maud/pull/306)

## [0.22.3] - 2021-09-27

Expand Down
20 changes: 20 additions & 0 deletions docs/content/elements-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`, 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.
Expand Down
34 changes: 34 additions & 0 deletions maud/tests/basic_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,40 @@ fn empty_attributes_question_mark() {
assert_eq!(result.into_string(), "<input checked disabled>");
}

#[test]
fn optional_attribute_some() {
let result = html! { input value=[Some("value")]; };
assert_eq!(result.into_string(), r#"<input value="value">"#);
}

#[test]
fn optional_attribute_none() {
let result = html! { input value=[None as Option<&str>]; };
assert_eq!(result.into_string(), r#"<input>"#);
}

#[test]
fn optional_attribute_non_string_some() {
let result = html! { input value=[Some(42)]; };
assert_eq!(result.into_string(), r#"<input value="42">"#);
}

#[test]
fn optional_attribute_variable() {
let result = html! {
@let x = Some(42);
input value=[x];
};
assert_eq!(result.into_string(), r#"<input value="42">"#);
}

#[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!" } } };
Expand Down
2 changes: 2 additions & 0 deletions maud_macros/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,15 @@ impl Attribute {
#[derive(Debug)]
pub enum AttrType {
Normal { value: Markup },
Optional { toggler: Toggler },
Empty { toggler: Option<Toggler> },
}

impl AttrType {
fn span(&self) -> Option<SpanRange> {
match *self {
AttrType::Normal { ref value } => Some(value.span()),
AttrType::Optional { ref toggler } => Some(toggler.span()),
AttrType::Empty { ref toggler } => toggler.as_ref().map(Toggler::span),
}
}
Expand Down
16 changes: 16 additions & 0 deletions maud_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,22 @@ impl Generator {
self.markup(value, build);
build.push_str("\"");
}
AttrType::Optional { toggler } => build.push_tokens({
// `inner_value` is the unpacked Some() from `toggler.cond`, see below.
let markup = Markup::Splice {
expr: quote!(inner_value),
outer_span: toggler.cond_span,
};
let mut build = self.builder();
build.push_str(" ");
self.name(name, &mut build);
build.push_str("=\"");
self.markup(markup, &mut build);
build.push_str("\"");
let body = build.finish();
let cond = toggler.cond;
quote!(if let Some(inner_value) = #cond { #body })
}),
AttrType::Empty { toggler: None } => {
build.push_str(" ");
self.name(name, build);
Expand Down
13 changes: 8 additions & 5 deletions maud_macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.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::Attribute {
attribute: ast::Attribute {
name,
attr_type: ast::AttrType::Normal { value },
},
attribute: ast::Attribute { name, attr_type },
});
}
// Empty attribute (legacy syntax)
Expand Down

0 comments on commit b787c18

Please sign in to comment.