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 lambda-fairy#283.
  • Loading branch information
zopieux committed Oct 19, 2021
1 parent 057a231 commit 15236ea
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 2 deletions.
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 attributes to an element using the `attr=[value]` syntax (note the square
brackets) that are only rendered if the value is `Some<T>`, 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.
Expand Down
20 changes: 20 additions & 0 deletions maud/tests/basic_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ fn empty_attributes_question_mark() {
assert_eq!(result.into_string(), "<input checked disabled>");
}

#[test]
fn optional_attributes() {
let result = html! { input value=[Some("value")]; };
assert_eq!(result.into_string(), r#"<input value="value">"#);
let result = html! { input value=[Some(42)]; };
assert_eq!(result.into_string(), r#"<input value="42">"#);
let result = html! { input value=[None as Option<&str>]; };
assert_eq!(result.into_string(), r#"<input>"#);
let result = html! {
@let x = Some(42);
input value=[x];
};
assert_eq!(result.into_string(), r#"<input value="42">"#);
let result = html! {
@let x: Option<u8> = None;
input value=[x];
};
assert_eq!(result.into_string(), r#"<input>"#);
}

#[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 { cond: TokenStream, value: Markup },
Empty { toggler: Option<Toggler> },
}

impl AttrType {
fn span(&self) -> Option<SpanRange> {
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),
}
}
Expand Down
20 changes: 20 additions & 0 deletions maud_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 26 additions & 2 deletions maud_macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
}
Expand Down Expand Up @@ -688,6 +694,24 @@ impl Parser {
}
}

// Parses the `[optional value]` syntax after an attribute declaration.
fn optional_attr(&mut self) -> Option<ast::AttrType> {
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<TokenStream> {
let mut result = Vec::new();
Expand Down

0 comments on commit 15236ea

Please sign in to comment.