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, } }