Skip to content

Commit

Permalink
Add support for Option<T> attributes (#306)
Browse files Browse the repository at this point in the history
* Add support for Option<T> attributes

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.

* Call `Generator::splice` directly

* Handle struct literals (edge case)

Co-authored-by: Chris Wong <xbuns@google.com>
  • Loading branch information
zopieux and lambda-fairy authored Oct 29, 2021
1 parent 057a231 commit 2909e51
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 39 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
41 changes: 41 additions & 0 deletions maud/tests/basic_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,47 @@ 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 x = Some(42);
let result = html! { 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 optional_attribute_braces() {
struct Pony {
cuteness: Option<i32>,
}
let result = html! { input value=[Pony { cuteness: Some(9000) }.cuteness]; };
assert_eq!(result.into_string(), r#"<input value="9000">"#);
}

#[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
63 changes: 29 additions & 34 deletions maud_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl Generator {
}
Markup::Literal { content, .. } => build.push_escaped(&content),
Markup::Symbol { symbol } => self.name(symbol, build),
Markup::Splice { expr, .. } => build.push_tokens(self.splice(expr)),
Markup::Splice { expr, .. } => self.splice(expr, build),
Markup::Element { name, attrs, body } => self.element(name, attrs, body, build),
Markup::Let { tokens, .. } => build.push_tokens(tokens),
Markup::Special { segments } => {
Expand Down Expand Up @@ -89,12 +89,13 @@ impl Generator {
TokenStream::from(block)
}

fn splice(&self, expr: TokenStream) -> TokenStream {
fn splice(&self, expr: TokenStream, build: &mut Builder) {
let output_ident = self.output_ident.clone();
quote!({
let tokens = quote!({
use maud::render::{RenderInternal, RenderWrapper};
RenderWrapper(&#expr).__maud_render_to(&mut #output_ident);
})
});
build.push_tokens(tokens);
}

fn element(&self, name: TokenStream, attrs: Vec<Attr>, body: ElementBody, build: &mut Builder) {
Expand Down Expand Up @@ -124,21 +125,35 @@ impl Generator {
self.markup(value, build);
build.push_str("\"");
}
AttrType::Optional {
toggler: Toggler { cond, .. },
} => {
let inner_value = quote!(inner_value);
let body = {
let mut build = self.builder();
build.push_str(" ");
self.name(name, &mut build);
build.push_str("=\"");
self.splice(inner_value.clone(), &mut build);
build.push_str("\"");
build.finish()
};
build.push_tokens(quote!(if let Some(#inner_value) = (#cond) { #body }));
}
AttrType::Empty { toggler: None } => {
build.push_str(" ");
self.name(name, build);
}
AttrType::Empty {
toggler: Some(toggler),
toggler: Some(Toggler { cond, .. }),
} => {
let head = desugar_toggler(toggler);
build.push_tokens({
let body = {
let mut build = self.builder();
build.push_str(" ");
self.name(name, &mut build);
let body = build.finish();
quote!(#head { #body })
})
build.finish()
};
build.push_tokens(quote!(if (#cond) { #body }));
}
}
}
Expand Down Expand Up @@ -196,16 +211,16 @@ fn desugar_classes_or_ids(
for name in values_static {
markups.extend(prepend_leading_space(name, &mut leading_space));
}
for (name, toggler) in values_toggled {
for (name, Toggler { cond, cond_span }) in values_toggled {
let body = Block {
markups: prepend_leading_space(name, &mut leading_space),
outer_span: toggler.cond_span,
// TODO: is this correct?
outer_span: cond_span,
};
let head = desugar_toggler(toggler);
markups.push(Markup::Special {
segments: vec![Special {
at_span: SpanRange::call_site(),
head,
head: quote!(if (#cond)),
body,
}],
});
Expand Down Expand Up @@ -234,26 +249,6 @@ fn prepend_leading_space(name: Markup, leading_space: &mut bool) -> Vec<Markup>
markups
}

fn desugar_toggler(
Toggler {
mut cond,
cond_span,
}: Toggler,
) -> TokenStream {
// If the expression contains an opening brace `{`,
// wrap it in parentheses to avoid parse errors
if cond.clone().into_iter().any(is_braced_block) {
let mut wrapped_cond = TokenTree::Group(Group::new(Delimiter::Parenthesis, cond));
wrapped_cond.set_span(cond_span.collapse());
cond = TokenStream::from(wrapped_cond);
}
quote!(if #cond)
}

fn is_braced_block(token: TokenTree) -> bool {
matches!(token, TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace)
}

////////////////////////////////////////////////////////

struct Builder {
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 2909e51

Please sign in to comment.