From dca04006921cd070690db2ea5f66dcc770dcd302 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Thu, 29 Jan 2015 13:47:11 +1300
Subject: [PATCH] Implement toggleable boolean attributes

Closes #4
---
 maud/src/lib.rs            |  4 ++--
 maud_macros/src/parse.rs   | 44 +++++++++++++++++++++++++-------------
 maud_macros/src/render.rs  | 22 +++++++++++++++++--
 maud_macros/tests/tests.rs | 18 +++++++++++++++-
 4 files changed, 68 insertions(+), 20 deletions(-)

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]