Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Option<T> attributes #306

Merged
merged 3 commits into from
Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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