Repository: a-h/templ (go)

Issue:
Broken attribute value after `templ fmt .`
**Describe the bug**
When running `templ fmt .` the escaped quotes (`&quot;`) become unescaped, breaking the HTML and prevents generation from the template.

**To Reproduce**
`test.templ`
```templ
package templates

templ test() {
	<div data-line-data="[{&quot;x&quot;: &quot;2025-05-18&quot;, &quot;y&quot;: 5 }, { &quot;x&quot;: &quot;2025-05-19&quot;, &quot;y&quot;: 30 }]"></div>
}
```
- Run `templ fmt .`

After:
`test.templ`
```templ
package templates

templ test() {
	<div data-line-data="[{"x": "2025-05-18", "y": 5 }, { "x": "2025-05-19", "y": 30 }]"></div>
}
```

**Expected behavior**
Leave the escaped characters in the attribute values escaped. The `templ fmt .` command should not have changed the example file.

**Screenshots**
Before formatting:

![Image](https://github.com/user-attachments/assets/e4fef41b-083b-464a-88b5-fbd6f82374a7)

After formatting:

![Image](https://github.com/user-attachments/assets/66c2ac43-2231-4e32-997b-5aaf28306a77)

**`templ info` output**
Run `templ info` and include the output.
```
go tool templ info   
(✓) os [ goos=windows goarch=amd64 ]
(✓) go [ location=C:\Program Files\Go\bin\go.exe version=go version go1.24.0 windows/amd64 ]
(✓) gopls [ location=C:\Users\Holo\go\bin\gopls.exe version=golang.org/x/tools/gopls v0.18.1 ]
(✗) templ [ location=C:\Users\Holo\go\bin\templ.exe version=v0.2.663 message=version mismatch - you're running "v0.3.865" at the command line, but the version in the path is "v0.2.663" ]
```

**Desktop (please complete the following information):**
 - OS: Windows
 - templ CLI version (`templ version`): `v0.3.865`
- Go version (`go version`): `go version go1.24.0 windows/amd64`
- `gopls` version (`gopls version`): `golang.org/x/tools/gopls v0.18.1`

**Additional context**
I am running templ from the `go tool` command (`go tool templ fmt .`)


Pull request:
fix: write constant attributes as they are in the source templ file
Closes #1153, replaces #1154

Patch:
diff --git a/.version b/.version
index 0e32e8c..dc3fada 100644
--- a/.version
+++ b/.version
@@ -1,1 +1,1 @@
-0.3.871
\ No newline at end of file
+0.0.882
\ No newline at end of file
diff --git a/benchmarks/templ/template_templ.go b/benchmarks/templ/template_templ.go
index 6fb43e2..c5f68b1 100644
--- a/benchmarks/templ/template_templ.go
+++ b/benchmarks/templ/template_templ.go
@@ -41,7 +41,7 @@ func Render(p Person) templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div style=\"font-family: &#39;sans-serif&#39;\" id=\"test\" data-contents=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div style=\"font-family: 'sans-serif'\" id=\"test\" data-contents=\"")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/generator.go b/generator/generator.go
index 089903b..0891cdd 100644
--- a/generator/generator.go
+++ b/generator/generator.go
@@ -1165,9 +1165,12 @@ func (g *generator) writeConstantAttribute(indentLevel int, attr *parser.Constan
 	if err = g.writeAttributeKey(indentLevel, attr.Key); err != nil {
 		return err
 	}
-	value := html.EscapeString(attr.Value)
-	value = escapeQuotes(value)
-	if _, err = g.w.WriteStringLiteral(indentLevel, fmt.Sprintf(`=\"%s\"`, value)); err != nil {
+	quote := `"`
+	if attr.SingleQuote {
+		quote = "'"
+	}
+	value := escapeQuotes("=" + quote + attr.Value + quote)
+	if _, err = g.w.WriteStringLiteral(indentLevel, value); err != nil {
 		return err
 	}
 	return nil
diff --git a/parser/v2/elementparser.go b/parser/v2/elementparser.go
index fc2f34b..bc8cb71 100644
--- a/parser/v2/elementparser.go
+++ b/parser/v2/elementparser.go
@@ -2,7 +2,6 @@ package parser
 
 import (
 	"fmt"
-	"html"
 	"strings"
 
 	"github.com/a-h/parse"
@@ -168,8 +167,6 @@ var (
 			return attr, false, nil
 		}
 
-		attr.Value = html.UnescapeString(attr.Value)
-
 		// Only use single quotes if actually required, due to double quote in the value (prefer double quotes).
 		attr.SingleQuote = attr.SingleQuote && strings.Contains(attr.Value, "\"")
 


Test patch:
diff --git a/generator/test-a-href/template_templ.go b/generator/test-a-href/template_templ.go
index 35d777a..7e1416c 100644
--- a/generator/test-a-href/template_templ.go
+++ b/generator/test-a-href/template_templ.go
@@ -28,7 +28,7 @@ func render() templ.Component {
 			templ_7745c5c3_Var1 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"javascript:alert(&#39;unaffected&#39;);\">Ignored</a> <a href=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"javascript:alert('unaffected');\">Ignored</a> <a href=\"")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-attribute-escaping/template_templ.go b/generator/test-attribute-escaping/template_templ.go
index 3afabeb..fbd7917 100644
--- a/generator/test-attribute-escaping/template_templ.go
+++ b/generator/test-attribute-escaping/template_templ.go
@@ -37,7 +37,7 @@ func BasicTemplate(url string) templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">text</a></div><div><button hx-post=\"/click\" hx-trigger=\"click\" hx-vals=\"{&#34;val&#34;:&#34;Value&#34;}\">Click</button></div>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">text</a></div><div><button hx-post=\"/click\" hx-trigger=\"click\" hx-vals='{\"val\":\"Value\"}'>Click</button></div>")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-complex-attributes/template_templ.go b/generator/test-complex-attributes/template_templ.go
index 7e4419a..533d526 100644
--- a/generator/test-complex-attributes/template_templ.go
+++ b/generator/test-complex-attributes/template_templ.go
@@ -28,7 +28,7 @@ func ComplexAttributes() templ.Component {
 			templ_7745c5c3_Var1 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"{darkMode: localStorage.getItem(&#39;darkMode&#39;) || localStorage.setItem(&#39;darkMode&#39;, &#39;system&#39;)}\" x-init=\"$watch(&#39;darkMode&#39;, val =&gt; localStorage.setItem(&#39;darkMode&#39;, val))\" :class=\"{&#39;dark&#39;: darkMode === &#39;dark&#39; || (darkMode === &#39;system&#39; &amp;&amp; window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches)}\"></div><div x-data=\"{ count: 0 }\"><button x-on:click=\"count++\">Increment</button> <span x-text=\"count\"></span></div><div x-data=\"{ count: 0 }\"><button @click=\"count++\">Increment</button> <span x-text=\"count\"></span></div>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"{darkMode: localStorage.getItem('darkMode') || localStorage.setItem('darkMode', 'system')}\" x-init=\"$watch('darkMode', val => localStorage.setItem('darkMode', val))\" :class=\"{'dark': darkMode === 'dark' || (darkMode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)}\"></div><div x-data=\"{ count: 0 }\"><button x-on:click=\"count++\">Increment</button> <span x-text=\"count\"></span></div><div x-data=\"{ count: 0 }\"><button @click=\"count++\">Increment</button> <span x-text=\"count\"></span></div>")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-form-action/template_templ.go b/generator/test-form-action/template_templ.go
index 98fc64e..2fa9baa 100644
--- a/generator/test-form-action/template_templ.go
+++ b/generator/test-form-action/template_templ.go
@@ -28,7 +28,7 @@ func render() templ.Component {
 			templ_7745c5c3_Var1 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form action=\"javascript:alert(&#39;unaffected&#39;);\">Ignored</form><form action=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form action=\"javascript:alert('unaffected');\">Ignored</form><form action=\"")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-html/template_templ.go b/generator/test-html/template_templ.go
index a939a65..cf44f45 100644
--- a/generator/test-html/template_templ.go
+++ b/generator/test-html/template_templ.go
@@ -41,7 +41,7 @@ func render(p person) templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div style=\"font-family: &#39;sans-serif&#39;\" id=\"test\" data-contents=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div style=\"font-family: 'sans-serif'\" id=\"test\" data-contents=\"")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-once/template_templ.go b/generator/test-once/template_templ.go
index 780684a..ff55bc5 100644
--- a/generator/test-once/template_templ.go
+++ b/generator/test-once/template_templ.go
@@ -78,7 +78,7 @@ func hello(label, name string) templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" onclick=\"hello(this.getAttribute(&#39;data-name&#39;))\">")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" onclick=\"hello(this.getAttribute('data-name'))\">")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-script-usage-nonce/template_templ.go b/generator/test-script-usage-nonce/template_templ.go
index 1e5c41b..91c258d 100644
--- a/generator/test-script-usage-nonce/template_templ.go
+++ b/generator/test-script-usage-nonce/template_templ.go
@@ -140,7 +140,7 @@ func ThreeButtons() templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button onMouseover=\"console.log(&#39;mouseover&#39;)\" type=\"button\">Button C</button> <button hx-on::click=\"alert(&#39;clicked inline&#39;)\" type=\"button\">Button D</button> ")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button onMouseover=\"console.log('mouseover')\" type=\"button\">Button C</button> <button hx-on::click=\"alert('clicked inline')\" type=\"button\">Button D</button> ")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/generator/test-script-usage/template_templ.go b/generator/test-script-usage/template_templ.go
index 66bfba4..1c274e6 100644
--- a/generator/test-script-usage/template_templ.go
+++ b/generator/test-script-usage/template_templ.go
@@ -150,7 +150,7 @@ func ThreeButtons() templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button onMouseover=\"console.log(&#39;mouseover&#39;)\" type=\"button\">Button C</button> <button hx-on::click=\"alert(&#39;clicked inline&#39;)\" type=\"button\">Button D</button> ")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button onMouseover=\"console.log('mouseover')\" type=\"button\">Button C</button> <button hx-on::click=\"alert('clicked inline')\" type=\"button\">Button D</button> ")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/parser/v2/elementparser_test.go b/parser/v2/elementparser_test.go
index fe44ceb..aee8153 100644
--- a/parser/v2/elementparser_test.go
+++ b/parser/v2/elementparser_test.go
@@ -583,11 +583,27 @@ if test {` + " " + `
 			},
 		},
 		{
-			name:   "attribute containing escaped text",
-			input:  ` href="&lt;&quot;&gt;"`,
+			name:   "constant attribute values are parsed as-is (single quoted)",
+			input:  ` href='"'`,
 			parser: StripType(constantAttributeParser),
 			expected: &ConstantAttribute{
-				Value: `<">`,
+				Value:       `"`,
+				SingleQuote: true,
+				Key: ConstantAttributeKey{
+					Name: "href",
+					NameRange: Range{
+						From: Position{Index: 1, Line: 0, Col: 1},
+						To:   Position{Index: 5, Line: 0, Col: 5},
+					},
+				},
+			},
+		},
+		{
+			name:   "constant attribute values are parsed as-is (double quoted)",
+			input:  ` href="&lt;'"`,
+			parser: StripType(constantAttributeParser),
+			expected: &ConstantAttribute{
+				Value: `&lt;'`,
 				Key: ConstantAttributeKey{
 					Name: "href",
 					NameRange: Range{
@@ -1915,3 +1931,64 @@ func FuzzElement(f *testing.F) {
 		_, _, _ = element.Parse(parse.NewInput(input))
 	})
 }
+
+func TestElementFormatting(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "element can be formatted",
+			input:    `<div>Text</div>`,
+			expected: `<div>Text</div>`,
+		},
+		{
+			name:     "attribute escaping is not lost",
+			input:    `<div data-line-data="[{&quot;x&quot;: 0, &quot;y&quot;: 1 }]"></div>`,
+			expected: `<div data-line-data="[{&quot;x&quot;: 0, &quot;y&quot;: 1 }]"></div>`,
+		},
+		{
+			name:     "constant attributes are not re-escaped - single quote",
+			input:    `<div data-line-data='"'></div>`,
+			expected: `<div data-line-data='"'></div>`,
+		},
+		{
+			name:     "constant attributes are not re-escaped - double quote",
+			input:    `<div data-line-data='"'></div>`,
+			expected: `<div data-line-data='"'></div>`,
+		},
+		{
+			name:  "constant attributes are not re-escaped - backslash",
+			input: `<div data-line-data='\'></div>`,
+			// templ formatting prefers double quotes.
+			expected: `<div data-line-data="\"></div>`,
+		},
+		{
+			name:  "constant attributes are not re-escaped - ampersand",
+			input: `<div data-line-data='&'></div>`,
+			// templ formatting prefers double quotes.
+			expected: `<div data-line-data="&"></div>`,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			input := parse.NewInput(tt.input)
+			result, ok, err := element.Parse(input)
+			if err != nil {
+				t.Fatalf("parser error: %v", err)
+			}
+			if !ok {
+				t.Fatalf("failed to parse at %d", input.Index())
+			}
+			formatted := new(strings.Builder)
+			if err = result.Write(formatted, 0); err != nil {
+				t.Fatalf("failed to format: %v", err)
+			}
+			if diff := cmp.Diff(tt.expected, formatted.String()); diff != "" {
+				t.Error(diff)
+			}
+		})
+	}
+}