From e273d897cf2d93210c05014bc822dfc934bd37ec Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Mon, 13 Aug 2018 15:53:34 -0700
Subject: [PATCH] Parse arbitrary expressions in classes and IDs

Closes #128
---
 maud/tests/basic_syntax.rs       | 22 +++++++++++++++++++++-
 maud/tests/control_structures.rs | 16 ++++++++++++++++
 maud/tests/splices.rs            | 21 +++++++++++++++++++++
 maud_macros/src/parse.rs         | 31 ++++++++++++++-----------------
 4 files changed, 72 insertions(+), 18 deletions(-)

diff --git a/maud/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs
index b94fe58..d6ae8ef 100644
--- a/maud/tests/basic_syntax.rs
+++ b/maud/tests/basic_syntax.rs
@@ -149,6 +149,12 @@ fn hyphens_in_class_names() {
     assert_eq!(s, r#"<p class="rocks-these are--my--rocks">yes</p>"#);
 }
 
+#[test]
+fn class_string() {
+    let s = html!(h1."pinkie-123" { "Pinkie Pie" }).into_string();
+    assert_eq!(s, r#"<h1 class="pinkie-123">Pinkie Pie</h1>"#);
+}
+
 #[test]
 fn toggle_classes() {
     fn test(is_cupcake: bool, is_muffin: bool) -> Markup {
@@ -167,6 +173,14 @@ fn toggle_classes_braces() {
     assert_eq!(s, r#"<p class="rocks">Awesome!</p>"#);
 }
 
+#[test]
+fn toggle_classes_string() {
+    let is_cupcake = true;
+    let is_muffin = false;
+    let s = html!(p."cupcake"[is_cupcake]."is_muffin"[is_muffin] { "Testing!" }).into_string();
+    assert_eq!(s, r#"<p class="cupcake">Testing!</p>"#);
+}
+
 #[test]
 fn mixed_classes() {
     fn test(is_muffin: bool) -> Markup {
@@ -177,11 +191,17 @@ fn mixed_classes() {
 }
 
 #[test]
-fn ids_shorthand() {
+fn id_shorthand() {
     let s = html!(p { "Hi, " span#thing { "Lyra" } "!" }).into_string();
     assert_eq!(s, r#"<p>Hi, <span id="thing">Lyra</span>!</p>"#);
 }
 
+#[test]
+fn id_string() {
+    let s = html!(h1#"pinkie-123" { "Pinkie Pie" }).into_string();
+    assert_eq!(s, r#"<h1 id="pinkie-123">Pinkie Pie</h1>"#);
+}
+
 #[test]
 fn classes_attrs_ids_mixed_up() {
     let s = html!(p { "Hi, " span.name.here lang="en" #thing { "Lyra" } "!" }).into_string();
diff --git a/maud/tests/control_structures.rs b/maud/tests/control_structures.rs
index b38a6e2..52060c8 100644
--- a/maud/tests/control_structures.rs
+++ b/maud/tests/control_structures.rs
@@ -26,6 +26,22 @@ fn if_expr() {
     }
 }
 
+#[test]
+fn if_expr_in_class() {
+    for &(chocolate_milk, expected) in &[
+        (0, r#"<p class="empty">Chocolate milk</p>"#),
+        (1, r#"<p class="full">Chocolate milk</p>"#),
+    ]
+    {
+        let s = html! {
+            p.@if chocolate_milk == 0 { "empty" } @else { "full" } {
+                "Chocolate milk"
+            }
+        }.into_string();
+        assert_eq!(s, expected);
+    }
+}
+
 #[test]
 fn if_let() {
     for &(input, output) in &[(Some("yay"), "yay"), (None, "oh noes")] {
diff --git a/maud/tests/splices.rs b/maud/tests/splices.rs
index 0974e2a..8781f37 100644
--- a/maud/tests/splices.rs
+++ b/maud/tests/splices.rs
@@ -42,6 +42,27 @@ fn attributes() {
     assert_eq!(s, r#"<img src="pinkie.jpg" alt="Pinkie Pie">"#);
 }
 
+#[test]
+fn class_shorthand() {
+    let pinkie_class = "pinkie";
+    let s = html!(p.(pinkie_class) { "Fun!" }).into_string();
+    assert_eq!(s, r#"<p class="pinkie">Fun!</p>"#);
+}
+
+#[test]
+fn class_shorthand_block() {
+    let class_prefix = "pinkie-";
+    let s = html!(p.{ (class_prefix) "123" } { "Fun!" }).into_string();
+    assert_eq!(s, r#"<p class="pinkie-123">Fun!</p>"#);
+}
+
+#[test]
+fn id_shorthand() {
+    let pinkie_id = "pinkie";
+    let s = html!(p#(pinkie_id) { "Fun!" }).into_string();
+    assert_eq!(s, r#"<p id="pinkie">Fun!</p>"#);
+}
+
 static BEST_PONY: &'static str = "Pinkie Pie";
 
 #[test]
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 43b36e6..7e511b0 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -149,7 +149,9 @@ impl Parser {
             },
             // Element
             TokenTree::Ident(_) => {
-                let name = self.namespaced_name()?;
+                // `.try_namespaced_name()` should never fail as we've
+                // already seen an `Ident`
+                let name = self.try_namespaced_name().expect("identifier");
                 self.element(name)?
             },
             // Splice
@@ -544,16 +546,14 @@ impl Parser {
                 // Class shorthand
                 (None, Some(TokenTree::Punct(ref punct))) if punct.as_char() == '.' => {
                     self.commit(attempt);
-                    // TODO parse arbitrary expressions here
-                    let name = ast::Markup::Symbol { symbol: self.name()? };
+                    let name = self.class_or_id_name()?;
                     let toggler = self.attr_toggler();
                     attrs.push(ast::Attr::Class { dot_span: punct.span(), name, toggler });
                 },
                 // ID shorthand
                 (None, Some(TokenTree::Punct(ref punct))) if punct.as_char() == '#' => {
                     self.commit(attempt);
-                    // TODO parse arbitrary expressions here
-                    let name = ast::Markup::Symbol { symbol: self.name()? };
+                    let name = self.class_or_id_name()?;
                     attrs.push(ast::Attr::Id { hash_span: punct.span(), name });
                 },
                 // If it's not a valid attribute, backtrack and bail out
@@ -598,6 +598,15 @@ impl Parser {
         Ok(attrs)
     }
 
+    /// Parses the name of a class or ID.
+    fn class_or_id_name(&mut self) -> ParseResult<ast::Markup> {
+        if let Some(symbol) = self.try_name() {
+            Ok(ast::Markup::Symbol { symbol })
+        } else {
+            self.markup()
+        }
+    }
+
     /// Parses the `[cond]` syntax after an empty attribute or class shorthand.
     fn attr_toggler(&mut self) -> Option<ast::Toggler> {
         match self.peek() {
@@ -613,12 +622,6 @@ impl Parser {
     }
 
     /// Parses an identifier, without dealing with namespaces.
-    fn name(&mut self) -> ParseResult<TokenStream> {
-        self.try_name().ok_or_else(|| {
-            Span::call_site().error("expected identifier").emit();
-        })
-    }
-
     fn try_name(&mut self) -> Option<TokenStream> {
         let mut result = Vec::new();
         if let Some(token @ TokenTree::Ident(_)) = self.peek() {
@@ -648,12 +651,6 @@ impl Parser {
 
     /// Parses a HTML element or attribute name, along with a namespace
     /// if necessary.
-    fn namespaced_name(&mut self) -> ParseResult<TokenStream> {
-        self.try_namespaced_name().ok_or_else(|| {
-            Span::call_site().error("expected identifier").emit();
-        })
-    }
-
     fn try_namespaced_name(&mut self) -> Option<TokenStream> {
         let mut result = vec![self.try_name()?];
         if let Some(TokenTree::Punct(ref punct)) = self.peek() {