Repository: lambda-fairy/maud
Pull request #396 (2023-10-19 22:33:51)

Accept literals in attribute names
The parser now accepts `TokenTree::Literal` as names for attributes.

To prevent the `name_to_string` function from showing and escaping the surrounding `"` of a string literal the string representation is trimmed first. This should not affect how other literals work which means that both `"name"="true"` and `123="true"` would be valid.

This also addresses the PR request in #194

fraschm1998: Tried this, I seem to get:

`"x-on:click"="toggle" type="button" aria-controls="mobile-menu" aria-expanded      ="false" {     ■ element body must be wrapped in braces      = help: see https://github.com/lambda-fairy/maud/pull/137 for details`
RedPhoenixQ: Could you send more context like the macro where this element is created. This looks like and issue with missing `{}` for the element body or `;` for a self closing element
fraschm1998: > Could you send more context like the macro where this element is created. This looks like and issue with missing `{}` for the element body or `;` for a self closing element

```
div."absolute inset-y-0 left-0 flex items-center sm:hidden" {                                                                                                                                                                                                                        
    1                                             // Mobile menu button                                                                                                                                                                                                                                                            
💥  2                                             button."relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500" "@click"="toggle" type="button" aria-controls="mobile-menu" aria-expanded="fa      lse" {     ■ element body must be wrapped in braces      = help: see https://github.com/lambda-fairy/maud/pull/137 for details                    
    3                                                 span."absolute -inset-0.5" {}                                                                                                                                                                                                                  
    4                                                 span."sr-only" { "Open main menu" }                                                                                                                                                                                                            
    5                                                 // Icon when menu is closed.                                                                                                                                                                                                                   
    6                                                 @for (item, path) in [("block", "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"), ("hidden", "M6 18L18 6M6 6l12 12")] {                                                                                                                         
    7                                                     svg.{ (item) " h-6 w-6" } fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" {                                                                                                                                                  
    8                                                         path stroke-linecap="round" stroke-linejoin="round" d=(path) {}                                                                                                                                                                                                      
    9                                                     }                                                                                                                                                                                                                                                                        
   10                                                 }                                                                                                                                                                                                                                                                            
   11                                             }                                                                                                                                                                                                                                                                                
   12                                         } 
   ```
RedPhoenixQ: I'm unable to reproduce the issue when copying the elements you sent. On the code that includes this PR it compiles for me and seemingly give the correct html.

This is the html string your code produces:
`"<div class=\"absolute inset-y-0 left-0 flex items-center sm:hidden\"><button class=\"relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500\" @click=\"toggle\" type=\"button\" aria-controls=\"mobile-menu\" aria-expanded=\"false\"><span class=\"absolute -inset-0.5\"></span><span class=\"sr-only\">Open main menu</span><svg class=\"block h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" aria-hidden=\"true\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5\"></path></svg><svg class=\"hidden h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" aria-hidden=\"true\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"></path></svg></button></div><h1 class=\"pinkie-123 asdf asdf asdf asdf asdf\">Pinkie Pie<button class=\"asdf\">test</button></h1>"`
fraschm1998: Nvm. Seems to be working now. I think I may have just forgotten to clear the build cache.

Thanks!
RedPhoenixQ: Are you using this PR locally? In your `Cargo.toml` you can replace `maud = X.X.X` with `maud = { git = "https://github.com/RedPhoenixQ/maud/tree/literals_in_attributes" }` and that should let you compile your code 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1d9102..60f636d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
   [#377](https://github.com/lambda-fairy/maud/pull/377)
 - Implement `Render` for `Arc<T>`
   [#380](https://github.com/lambda-fairy/maud/pull/380)
+- Accept literals in attribute names
+  [#396](https://github.com/lambda-fairy/maud/pull/396)
 
 ## [0.25.0] - 2023-04-16
 
diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs
index cd8a2ce..16d228e 100644
--- a/maud_macros/src/ast.rs
+++ b/maud_macros/src/ast.rs
@@ -1,5 +1,6 @@
 use proc_macro2::{TokenStream, TokenTree};
 use proc_macro_error::SpanRange;
+use syn::Lit;
 
 #[derive(Debug)]
 pub enum Markup {
@@ -217,5 +218,21 @@ pub fn join_ranges<I: IntoIterator<Item = SpanRange>>(ranges: I) -> SpanRange {
 }
 
 pub fn name_to_string(name: TokenStream) -> String {
-    name.into_iter().map(|token| token.to_string()).collect()
+    name.into_iter()
+        .map(|token| {
+            if let TokenTree::Literal(literal) = token {
+                match Lit::new(literal.clone()) {
+                    Lit::Str(str) => str.value(),
+                    Lit::Char(char) => char.value().to_string(),
+                    Lit::ByteStr(byte) => {
+                        String::from_utf8(byte.value()).expect("Invalid utf8 byte")
+                    }
+                    Lit::Byte(byte) => (byte.value() as char).to_string(),
+                    _ => literal.to_string(),
+                }
+            } else {
+                token.to_string()
+            }
+        })
+        .collect()
 }
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 3521ce4..662b830 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -702,12 +702,13 @@ impl Parser {
     /// Parses an identifier, without dealing with namespaces.
     fn try_name(&mut self) -> Option<TokenStream> {
         let mut result = Vec::new();
-        if let Some(token @ TokenTree::Ident(_)) = self.peek() {
-            self.advance();
-            result.push(token);
-        } else {
-            return None;
-        }
+        match self.peek() {
+            Some(token @ TokenTree::Ident(_)) | Some(token @ TokenTree::Literal(_)) => {
+                self.advance();
+                result.push(token);
+            }
+            _ => return None,
+        };
         let mut expect_ident = false;
         loop {
             expect_ident = match self.peek() {


Test patch:
diff --git a/maud/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs
index b84f795..96e0235 100644
--- a/maud/tests/basic_syntax.rs
+++ b/maud/tests/basic_syntax.rs
@@ -181,6 +181,34 @@ fn hyphens_in_attribute_names() {
     );
 }
 
+#[test]
+fn string_literals_in_attribute_names() {
+    let result = html! { this "@sentence:-is.not"="false" of-course {} };
+    assert_eq!(
+        result.into_string(),
+        r#"<this @sentence:-is.not="false" of-course></this>"#
+    );
+}
+
+#[test]
+fn raw_string_literals_in_attribute_names() {
+    let result = html! { this r#"@sentence:-is.not"#="false" of-course {} };
+    assert_eq!(
+        result.into_string(),
+        r#"<this @sentence:-is.not="false" of-course></this>"#
+    );
+}
+
+#[test]
+fn other_literals_in_attribute_names() {
+    let result =
+        html! { this b"byte_string"="false" 123="123" 2.5 true 'a'="a" b'b'="b" of-course {} };
+    assert_eq!(
+        result.into_string(),
+        r#"<this byte_string="false" 123="123" 2.5 true a="a" b="b" of-course></this>"#
+    );
+}
+
 #[test]
 fn class_shorthand() {
     let result = html! { p { "Hi, " span.name { "Lyra" } "!" } };