diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 85f1eb8..9ef2b45 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -99,7 +99,7 @@ //! html! { //! form method="POST" { //! label for="waffles" "Do you like waffles?" -//! input name="waffles" type="checkbox" checked=! / +//! input name="waffles" type="checkbox" checked? / //! } //! } //! ``` @@ -114,7 +114,7 @@ //! Add attributes using the syntax `attr="value"`. Attributes must be //! quoted: they are parsed as string literals. //! -//! To declare an empty attribute, use `!` for the value: `checked=!`. +//! To declare an empty attribute, use a `?` suffix: `checked?`. //! //! ## Splices //! diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 31de992..c69df18 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -20,6 +20,9 @@ macro_rules! eq { macro_rules! not { () => (TtToken(_, token::Not)) } +macro_rules! question { + () => (TtToken(_, token::Question)) +} macro_rules! semi { () => (TtToken(_, token::Semi)) } @@ -90,11 +93,13 @@ impl<'cx, 's, 'i, 'r, 'o> Parser<'cx, 's, 'i, 'r, 'o> { // Splice [ref tt @ dollar!(), dollar!(), ..] => { self.shift(2); - self.splice(Escape::PassThru, tt.get_span()) + let expr = self.splice(tt.get_span()); + self.render.splice(expr, Escape::PassThru); }, [ref tt @ dollar!(), ..] => { self.shift(1); - self.splice(Escape::Escape, tt.get_span()) + let expr = self.splice(tt.get_span()); + self.render.splice(expr, Escape::Escape); }, // Element [ident!(sp, name), ..] => { @@ -127,7 +132,7 @@ impl<'cx, 's, 'i, 'r, 'o> Parser<'cx, 's, 'i, 'r, 'o> { } } - fn splice(&mut self, escape: Escape, sp: Span) { + fn splice(&mut self, sp: Span) -> P<Expr> { let mut tts = vec![]; // First, munch a single token tree if let [ref tt, ..] = self.input { @@ -151,10 +156,9 @@ impl<'cx, 's, 'i, 'r, 'o> Parser<'cx, 's, 'i, 'r, 'o> { } } if tts.is_empty() { - self.render.cx.span_err(sp, "expected expression for this splice"); + self.render.cx.span_fatal(sp, "expected expression for this splice"); } else { - let expr = self.new_rust_parser(tts).parse_expr(); - self.render.splice(expr, escape); + self.new_rust_parser(tts).parse_expr() } } @@ -175,14 +179,10 @@ impl<'cx, 's, 'i, 'r, 'o> Parser<'cx, 's, 'i, 'r, 'o> { } fn attrs(&mut self) { - while let [ident!(name), eq!(), ..] = self.input { - self.shift(2); - if let [not!(), ..] = self.input { - // Empty attribute - self.shift(1); - self.render.attribute_empty(name.as_str()); - } else { + loop { match self.input { + [ident!(name), eq!(), ..] => { // Non-empty attribute + self.shift(2); self.render.attribute_start(name.as_str()); { // Parse a value under an attribute context @@ -192,8 +192,22 @@ impl<'cx, 's, 'i, 'r, 'o> Parser<'cx, 's, 'i, 'r, 'o> { self.in_attr = old_in_attr; } self.render.attribute_end(); - } - } + }, + [ident!(name), question!(), ..] => { + // Empty attribute + self.shift(2); + if let [ref tt @ eq!(), ..] = self.input { + // Toggle the attribute based on a boolean expression + self.shift(1); + let expr = self.splice(tt.get_span()); + self.render.attribute_empty_if(name.as_str(), expr); + } else { + // Write the attribute unconditionally + self.render.attribute_empty(name.as_str()); + } + }, + _ => return, + }} } fn block(&mut self, tts: &[TokenTree]) { diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs index 0def710..09a2dd7 100644 --- a/maud_macros/src/render.rs +++ b/maud_macros/src/render.rs @@ -43,8 +43,13 @@ impl<'cx, 's, 'o> Renderer<'cx, 's, 'o> { /// Push an expression statement, also wrapping it with `try!`. fn push(&mut self, expr: P<Expr>) { - let expr = self.cx.stmt_expr(self.cx.expr_try(expr.span, expr)); - self.stmts.push(expr); + let stmt = self.make_stmt(expr); + self.stmts.push(stmt); + } + + /// Create an expression statement, also wrapping it with `try!`. + fn make_stmt(&mut self, expr: P<Expr>) -> P<Stmt> { + self.cx.stmt_expr(self.cx.expr_try(expr.span, expr)) } /// Append a literal pre-escaped string. @@ -94,6 +99,19 @@ impl<'cx, 's, 'o> Renderer<'cx, 's, 'o> { self.write(name); } + pub fn attribute_empty_if(&mut self, name: &str, expr: P<Expr>) { + let s: String = [" ", name].concat(); + let s = &s[]; + let w = self.w; + let expr = quote_expr!(self.cx, + if $expr { + $w.write_str($s) + } else { + Ok(()) + }); + self.push(expr); + } + pub fn attribute_end(&mut self) { self.write("\""); } diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs index de054db..6834094 100644 --- a/maud_macros/tests/tests.rs +++ b/maud_macros/tests/tests.rs @@ -76,7 +76,7 @@ mod elements { #[test] fn empty_attributes() { - let s = html! { div readonly=! input type="checkbox" checked=! / }.to_string(); + let s = html! { div readonly? input type="checkbox" checked? / }.to_string(); assert_eq!(s, r#"<div readonly><input type="checkbox" checked></div>"#); } } @@ -108,6 +108,22 @@ mod splices { assert_eq!(s, "3628800"); } + #[test] + fn attributes() { + let rocks = true; + let s = html! { + input checked?=true / + input checked?=false / + input checked?=rocks / + input checked?=(!rocks) / + }.to_string(); + assert_eq!(s, concat!( + r#"<input checked>"#, + r#"<input>"#, + r#"<input checked>"#, + r#"<input>"#)); + } + static BEST_PONY: &'static str = "Pinkie Pie"; #[test]