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

Make the ? suffix for empty attributes optional #238

Merged
merged 3 commits into from
Nov 11, 2020
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🎉
Expand Down
10 changes: 7 additions & 3 deletions docs/content/elements-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/splices-toggles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
24 changes: 15 additions & 9 deletions maud/tests/basic_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"<div readonly><input type="checkbox" checked></div>"#
Expand All @@ -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(),
Expand All @@ -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#"<input checked>"#);
}

#[test]
fn empty_attributes_question_mark() {
let result = html! { input checked? disabled?[true]; };
assert_eq!(result.into_string(), "<input checked disabled>");
}

#[test]
fn colons_in_names() {
let result = html! { pon-pon:controls-alpha { a on:click="yay()" { "Yay!" } } };
Expand All @@ -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#"<this sentence-is="false" of-course></this>"#
Expand Down Expand Up @@ -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#"<div class="awesome-class" id="unique-id" contenteditable dir="rtl"></div>"#
Expand All @@ -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#"<div class="awesome-class" id="unique-id" contenteditable dir="rtl"></div>"#
Expand Down
120 changes: 64 additions & 56 deletions maud_macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ast::Markup> {
let mut result = Vec::new();
Expand Down Expand Up @@ -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,
}
}

Expand Down