diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69fd788b..a156f7f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
## Unreleased
+- [Changed] Don't require `?` suffix for empty attributes. The old syntax is kept for backward compatibility.
+ [#238](https://github.com/lambda-fairy/maud/pull/238)
+
## [0.22.1] - 2020-11-02
- [Added] Stable support 🎉
diff --git a/docs/content/elements-attributes.md b/docs/content/elements-attributes.md
index a5f0f4f3..03b4da1e 100644
--- a/docs/content/elements-attributes.md
+++ b/docs/content/elements-attributes.md
@@ -64,20 +64,24 @@ html! {
}
```
-## Empty attributes: `checked?`
+## Empty attributes: `checked`
-Declare an empty attribute using a `?` suffix: `checked?`.
+Declare an empty attribute by omitting the value.
```rust
html! {
form {
- input type="checkbox" name="cupcakes" checked?;
+ input type="checkbox" name="cupcakes" checked;
" "
label for="cupcakes" { "Do you like cupcakes?" }
}
}
```
+Before version 0.22.2, Maud required a `?` suffix on empty attributes: `checked?`. This is no longer necessary ([#238]), but still supported for backward compatibility.
+
+[#238]: https://github.com/lambda-fairy/maud/pull/238
+
## Classes and IDs: `.foo` `#bar`
Add classes and IDs to an element using `.foo` and `#bar` syntax. You can chain multiple classes and IDs together, and mix and match them with other attributes:
diff --git a/docs/content/splices-toggles.md b/docs/content/splices-toggles.md
index 04823d40..ba4942a9 100644
--- a/docs/content/splices-toggles.md
+++ b/docs/content/splices-toggles.md
@@ -95,7 +95,7 @@ This works on empty attributes:
```rust
let allow_editing = true;
html! {
- p contenteditable?[allow_editing] {
+ p contenteditable[allow_editing] {
"Edit me, I "
em { "dare" }
" you."
diff --git a/maud/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs
index e99c8431..e7f73ca1 100644
--- a/maud/tests/basic_syntax.rs
+++ b/maud/tests/basic_syntax.rs
@@ -76,7 +76,7 @@ fn simple_attributes() {
#[test]
fn empty_attributes() {
- let result = html! { div readonly? { input type="checkbox" checked?; } };
+ let result = html! { div readonly { input type="checkbox" checked; } };
assert_eq!(
result.into_string(),
r#"
"#
@@ -87,10 +87,10 @@ fn empty_attributes() {
fn toggle_empty_attributes() {
let rocks = true;
let result = html! {
- input checked?[true];
- input checked?[false];
- input checked?[rocks];
- input checked?[!rocks];
+ input checked[true];
+ input checked[false];
+ input checked[rocks];
+ input checked[!rocks];
};
assert_eq!(
result.into_string(),
@@ -108,10 +108,16 @@ fn toggle_empty_attributes_braces() {
struct Maud {
rocks: bool,
}
- let result = html! { input checked?[Maud { rocks: true }.rocks]; };
+ let result = html! { input checked[Maud { rocks: true }.rocks]; };
assert_eq!(result.into_string(), r#""#);
}
+#[test]
+fn empty_attributes_question_mark() {
+ let result = html! { input checked? disabled?[true]; };
+ assert_eq!(result.into_string(), "");
+}
+
#[test]
fn colons_in_names() {
let result = html! { pon-pon:controls-alpha { a on:click="yay()" { "Yay!" } } };
@@ -133,7 +139,7 @@ fn hyphens_in_element_names() {
#[test]
fn hyphens_in_attribute_names() {
- let result = html! { this sentence-is="false" of-course? {} };
+ let result = html! { this sentence-is="false" of-course {} };
assert_eq!(
result.into_string(),
r#""#
@@ -281,7 +287,7 @@ fn div_shorthand_id() {
#[test]
fn div_shorthand_class_with_attrs() {
- let result = html! { .awesome-class contenteditable? dir="rtl" #unique-id {} };
+ let result = html! { .awesome-class contenteditable dir="rtl" #unique-id {} };
assert_eq!(
result.into_string(),
r#""#
@@ -290,7 +296,7 @@ fn div_shorthand_class_with_attrs() {
#[test]
fn div_shorthand_id_with_attrs() {
- let result = html! { #unique-id contenteditable? dir="rtl" .awesome-class {} };
+ let result = html! { #unique-id contenteditable dir="rtl" .awesome-class {} };
assert_eq!(
result.into_string(),
r#""#
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 6390969b..3a907e03 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -63,11 +63,6 @@ impl Parser {
self.next();
}
- /// Overwrites the current parser state with the given parameter.
- fn commit(&mut self, attempt: Parser) {
- *self = attempt;
- }
-
/// Parses and renders multiple blocks of markup.
fn markups(&mut self) -> Vec {
let mut result = Vec::new();
@@ -534,60 +529,73 @@ impl Parser {
fn attrs(&mut self) -> ast::Attrs {
let mut attrs = Vec::new();
loop {
- let mut attempt = self.clone();
- let maybe_name = attempt.try_namespaced_name();
- let token_after = attempt.next();
- match (maybe_name, token_after) {
- // Non-empty attribute
- (Some(ref name), Some(TokenTree::Punct(ref punct))) if punct.as_char() == '=' => {
- self.commit(attempt);
- let value;
- {
- // Parse a value under an attribute context
- let in_attr = mem::replace(&mut self.in_attr, true);
- value = self.markup();
- self.in_attr = in_attr;
+ if let Some(name) = self.try_namespaced_name() {
+ // Attribute
+ match self.peek() {
+ // Non-empty attribute
+ Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
+ self.advance();
+ let value;
+ {
+ // Parse a value under an attribute context
+ let in_attr = mem::replace(&mut self.in_attr, true);
+ value = self.markup();
+ self.in_attr = in_attr;
+ }
+ attrs.push(ast::Attr::Attribute {
+ attribute: ast::Attribute {
+ name,
+ attr_type: ast::AttrType::Normal { value },
+ },
+ });
+ }
+ // Empty attribute (legacy syntax)
+ Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => {
+ self.advance();
+ let toggler = self.attr_toggler();
+ attrs.push(ast::Attr::Attribute {
+ attribute: ast::Attribute {
+ name: name.clone(),
+ attr_type: ast::AttrType::Empty { toggler },
+ },
+ });
+ }
+ // Empty attribute (new syntax)
+ _ => {
+ let toggler = self.attr_toggler();
+ attrs.push(ast::Attr::Attribute {
+ attribute: ast::Attribute {
+ name: name.clone(),
+ attr_type: ast::AttrType::Empty { toggler },
+ },
+ });
}
- attrs.push(ast::Attr::Attribute {
- attribute: ast::Attribute {
- name: name.clone(),
- attr_type: ast::AttrType::Normal { value },
- },
- });
- }
- // Empty attribute
- (Some(ref name), Some(TokenTree::Punct(ref punct))) if punct.as_char() == '?' => {
- self.commit(attempt);
- let toggler = self.attr_toggler();
- attrs.push(ast::Attr::Attribute {
- attribute: ast::Attribute {
- name: name.clone(),
- attr_type: ast::AttrType::Empty { toggler },
- },
- });
- }
- // Class shorthand
- (None, Some(TokenTree::Punct(ref punct))) if punct.as_char() == '.' => {
- self.commit(attempt);
- let name = self.class_or_id_name();
- let toggler = self.attr_toggler();
- attrs.push(ast::Attr::Class {
- dot_span: SpanRange::single_span(punct.span()),
- name,
- toggler,
- });
}
- // ID shorthand
- (None, Some(TokenTree::Punct(ref punct))) if punct.as_char() == '#' => {
- self.commit(attempt);
- let name = self.class_or_id_name();
- attrs.push(ast::Attr::Id {
- hash_span: SpanRange::single_span(punct.span()),
- name,
- });
+ } else {
+ match self.peek() {
+ // Class shorthand
+ Some(TokenTree::Punct(ref punct)) if punct.as_char() == '.' => {
+ self.advance();
+ let name = self.class_or_id_name();
+ let toggler = self.attr_toggler();
+ attrs.push(ast::Attr::Class {
+ dot_span: SpanRange::single_span(punct.span()),
+ name,
+ toggler,
+ });
+ }
+ // ID shorthand
+ Some(TokenTree::Punct(ref punct)) if punct.as_char() == '#' => {
+ self.advance();
+ let name = self.class_or_id_name();
+ attrs.push(ast::Attr::Id {
+ hash_span: SpanRange::single_span(punct.span()),
+ name,
+ });
+ }
+ // If it's not a valid attribute, backtrack and bail out
+ _ => break,
}
- // If it's not a valid attribute, backtrack and bail out
- _ => break,
}
}