Repository: a-h/templ
Pull request #1197 (2025-06-17 13:16:50)

refactor: allow more flexible parsing of expression attributes
- **chore: bump version [no-ci]**
- **refactor: allow more flexible parsing of expression attributes**



Patch:
diff --git a/.version b/.version
index e4db128..44d1bea 100644
--- a/.version
+++ b/.version
@@ -1,1 +1,1 @@
-0.3.896
\ No newline at end of file
+0.3.903
\ No newline at end of file
diff --git a/parser/v2/elementparser.go b/parser/v2/elementparser.go
index 25f2152..c482a96 100644
--- a/parser/v2/elementparser.go
+++ b/parser/v2/elementparser.go
@@ -123,13 +123,13 @@ func (avp attributeValueParser) Parse(pi *parse.Input) (value string, ok bool, e
 var (
 	attributeValueParsers = []attributeValueParser{
 		// Double quoted.
-		{EqualsAndQuote: parse.String(`="`), Suffix: parse.String(`"`), UseSingleQuote: false},
+		{EqualsAndQuote: parse.StringFrom(parse.OptionalWhitespace, parse.String(`="`)), Suffix: parse.String(`"`), UseSingleQuote: false},
 		// Single quoted.
-		{EqualsAndQuote: parse.String(`='`), Suffix: parse.String(`'`), UseSingleQuote: true},
+		{EqualsAndQuote: parse.StringFrom(parse.OptionalWhitespace, parse.String(`='`)), Suffix: parse.String(`'`), UseSingleQuote: true},
 		// Unquoted.
 		// A valid unquoted attribute value in HTML is any string of text that is not an empty string,
 		// and that doesn’t contain spaces, tabs, line feeds, form feeds, carriage returns, ", ', `, =, <, or >.
-		{EqualsAndQuote: parse.String("="), Suffix: parse.Any(parse.RuneIn(" \t\n\r\"'`=<>/"), parse.EOF[string]()), UseSingleQuote: false},
+		{EqualsAndQuote: parse.StringFrom(parse.OptionalWhitespace, parse.String("=")), Suffix: parse.Any(parse.RuneIn(" \t\n\r\"'`=<>/"), parse.EOF[string]()), UseSingleQuote: false},
 	}
 	constantAttributeParser = parse.Func(func(pi *parse.Input) (attr *ConstantAttribute, ok bool, err error) {
 		start := pi.Index()
@@ -288,6 +288,8 @@ var boolExpressionAttributeParser = parse.Func(func(pi *parse.Input) (r *BoolExp
 	return r, true, nil
 })
 
+var expressionAttributeStartParser = parse.StringFrom(parse.OptionalWhitespace, parse.String("="), parse.OptionalWhitespace, parse.String("{"), parse.OptionalWhitespace)
+
 var expressionAttributeParser = parse.Func(func(pi *parse.Input) (attr *ExpressionAttribute, ok bool, err error) {
 	start := pi.Index()
 
@@ -305,7 +307,7 @@ var expressionAttributeParser = parse.Func(func(pi *parse.Input) (attr *Expressi
 	}
 
 	// ={
-	if _, ok, err = parse.Or(parse.String("={ "), parse.String("={")).Parse(pi); err != nil || !ok {
+	if _, ok, err = expressionAttributeStartParser.Parse(pi); err != nil || !ok {
 		pi.Seek(start)
 		return
 	}


Test patch:
diff --git a/cmd/templ/fmtcmd/testdata.txtar b/cmd/templ/fmtcmd/testdata.txtar
index 8041fce..a665b6a 100644
--- a/cmd/templ/fmtcmd/testdata.txtar
+++ b/cmd/templ/fmtcmd/testdata.txtar
@@ -52,3 +52,15 @@ templ c() {
 		</p>
 	</div>
 }
+-- d.templ --
+package test
+
+templ c(s string) {
+  <div s= {s}/>
+}
+-- d.templ --
+package test
+
+templ c(s string) {
+  <div s={ s }/>
+}
diff --git a/parser/v2/elementparser_test.go b/parser/v2/elementparser_test.go
index f2111e9..bc1d6c1 100644
--- a/parser/v2/elementparser_test.go
+++ b/parser/v2/elementparser_test.go
@@ -1791,6 +1791,43 @@ func TestElementParser(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:  "element: space before name= { is still parsed as an expression",
+			input: `<div hx-vals= { "test" }></div>`,
+			expected: &Element{
+				Name: "div",
+				NameRange: Range{
+					From: Position{Index: 1, Line: 0, Col: 1},
+					To:   Position{Index: 4, Line: 0, Col: 4},
+				},
+				Attributes: []Attribute{
+					&ExpressionAttribute{
+						Key: ConstantAttributeKey{
+							Name: "hx-vals",
+							NameRange: Range{
+								From: Position{Index: 5, Line: 0, Col: 5},
+								To:   Position{Index: 12, Line: 0, Col: 12},
+							},
+						},
+						Expression: Expression{
+							Value: `"test"`,
+							Range: Range{
+								From: Position{
+									Index: 16,
+									Line:  0,
+									Col:   16,
+								},
+								To: Position{
+									Index: 22,
+									Line:  0,
+									Col:   22,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {