Repository: a-h/templ (go)

Issue:
Generation gets stuck after update
After updating to [v0.3.850](https://github.com/a-h/templ/releases/tag/v0.3.850) `templ generate` would get stuck after generating a specific template. This happens both locally and in GitHub Actions. I've updated `go.mod` to the latest release as well.

Here's a sample verbose output:
```
$ ~/go/bin/templ generate -path ./templates -v
(✓) Walking directory [ path=/.../templates devMode=false ]
(✓) Starting event handler
(✓) Starting post-generation handler
(✓) Processing file [ file=/.../templates/A_templ.go ]
(✓) File not updated [ file=/.../templates/A_templ.go ]
...
(✓) Dev mode not enabled, process can finish early
...
(✓) Generated code [ file=/../templates/officetemplates/upsert_client.templ in=67.972917ms ]
(✓) File not updated [ file=/.../templates/officetemplates/upsert_client.templ ]
```

And generation would just get stuck here.

Reverting the executable and `go.mod` to [v0.3.833](https://github.com/a-h/templ/releases/tag/v0.3.833) fixes the issue for me:
```
$ git reset --hard HEAD~1 // resetting go.mod
$ rm ~/go/bin/templ
$ go install github.com/a-h/templ/cmd/templ@v0.3.833
$ ~/go/bin/templ generate -path ./templates -v
(✓) Starting event handler
(✓) Starting post-generation handler
...
(✓) Generated code [ file=/.../templates/officetemplates/upsert_client.templ in=60.899458ms ]
(✓) File not updated [ file=/.../templates/officetemplates/upsert_client.templ ]
(✓) Post-generation event channel closed, exiting
(✓) Waiting for push handler to complete
(✓) Waiting for event handler to complete
(✓) Waiting for post-generation handler to complete
(✓) Complete [ updates=0 duration=116.889459ms ]
```

Any way to debug what's going on?

EDIT: after looking at the logs again, it seems like it gets stuck after the very last file.

Pull request:
fix: incorrect parsing of HTML within JavaScript strings, fixes #1106

Patch:
diff --git a/.gitignore b/.gitignore
index 0420c67..8318e77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,8 @@ result
 # Editors
 ## nvim
 .null-ls*
+# vscode
+.vscode/
 
 # Go workspace.
 go.work
diff --git a/parser/v2/fuzz.sh b/parser/v2/fuzz.sh
index cee8f72..c74517c 100755
--- a/parser/v2/fuzz.sh
+++ b/parser/v2/fuzz.sh
@@ -1,2 +1,5 @@
+#!/bin/bash
 echo Element
 go test -fuzz=FuzzElement -fuzztime=120s
+echo Script
+go test -fuzz=FuzzScript -fuzztime=120s
diff --git a/parser/v2/goexpression/fuzz.sh b/parser/v2/goexpression/fuzz.sh
index 20062f9..08c800e 100755
--- a/parser/v2/goexpression/fuzz.sh
+++ b/parser/v2/goexpression/fuzz.sh
@@ -1,3 +1,4 @@
+#!/bin/bash
 echo If
 go test -fuzz=FuzzIf -fuzztime=120s
 echo For
diff --git a/parser/v2/scriptparser.go b/parser/v2/scriptparser.go
index d37618d..3fe6644 100644
--- a/parser/v2/scriptparser.go
+++ b/parser/v2/scriptparser.go
@@ -52,6 +52,7 @@ func (p scriptElementParser) Parse(pi *parse.Input) (n Node, ok bool, err error)
 	// Parse the contents, we should get script text or Go expressions up until the closing tag.
 	var sb strings.Builder
 	var isInsideStringLiteral bool
+
 loop:
 	for {
 		// Read and decide whether we're we've hit a:
@@ -70,6 +71,11 @@ loop:
 			break loop
 		}
 
+		if _, ok, err = endTagStart.Parse(pi); err != nil || ok {
+			// We've reached the end of the script, but the end tag is probably invalid.
+			break loop
+		}
+
 		var code Node
 		code, ok, err = goCodeInJavaScript.Parse(pi)
 		if err != nil {
@@ -91,6 +97,7 @@ loop:
 			continue loop
 		}
 
+		// Read JavaScript chracaters.
 		for {
 			before := pi.Index()
 			var c string
@@ -105,7 +112,11 @@ loop:
 				}
 				peeked, _ := pi.Peek(1)
 				peeked = c + peeked
-				if isEOF || peeked == "{{" || peeked == "</" || peeked == "//" || peeked == "/*" {
+
+				breakForGo := peeked == "{{"
+				breakForHTML := !isInsideStringLiteral && (peeked == "</" || peeked == "//" || peeked == "/*")
+
+				if isEOF || breakForGo || breakForHTML {
 					if sb.Len() > 0 {
 						e.Contents = append(e.Contents, NewScriptContentsJS(sb.String()))
 						sb.Reset()
@@ -128,6 +139,7 @@ loop:
 }
 
 var jsEndTag = parse.String("</script>")
+var endTagStart = parse.String("</")
 
 var jsCharacter = parse.Any(jsEscapedCharacter, parse.AnyRune)
 
@@ -136,7 +148,9 @@ var jsEscapedCharacter = parse.StringFrom(parse.String("\\"), parse.AnyRune)
 var jsComment = parse.Any(jsSingleLineComment, jsMultiLineComment)
 
 var jsStartSingleLineComment = parse.String("//")
-var jsSingleLineComment = parse.StringFrom(jsStartSingleLineComment, parse.StringUntil(parse.NewLine), parse.NewLine)
+var jsEndOfSingleLineComment = parse.StringFrom(parse.Or(parse.NewLine, parse.EOF[string]()))
+var jsSingleLineComment = parse.StringFrom(jsStartSingleLineComment, parse.StringUntil(jsEndOfSingleLineComment), jsEndOfSingleLineComment)
 
 var jsStartMultiLineComment = parse.String("/*")
-var jsMultiLineComment = parse.StringFrom(jsStartMultiLineComment, parse.StringUntil(parse.String("*/")), parse.String("*/"), parse.OptionalWhitespace)
+var jsEndOfMultiLineComment = parse.StringFrom(parse.Or(parse.String("*/"), parse.EOF[string]()))
+var jsMultiLineComment = parse.StringFrom(jsStartMultiLineComment, parse.StringUntil(jsEndOfMultiLineComment), jsEndOfMultiLineComment, parse.OptionalWhitespace)


Test patch:
diff --git a/parser/v2/scriptparser_test.go b/parser/v2/scriptparser_test.go
index 2082589..a952508 100644
--- a/parser/v2/scriptparser_test.go
+++ b/parser/v2/scriptparser_test.go
@@ -1,12 +1,55 @@
 package parser
 
 import (
+	"path/filepath"
 	"testing"
 
+	_ "embed"
+
 	"github.com/a-h/parse"
 	"github.com/google/go-cmp/cmp"
+	"golang.org/x/tools/txtar"
 )
 
+func TestScriptElementParserPlain(t *testing.T) {
+	files, _ := filepath.Glob("scriptparsertestdata/*.txt")
+	if len(files) == 0 {
+		t.Errorf("no test files found")
+	}
+	for _, file := range files {
+		t.Run(filepath.Base(file), func(t *testing.T) {
+			a, err := txtar.ParseFile(file)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(a.Files) != 2 {
+				t.Fatalf("expected 2 files, got %d", len(a.Files))
+			}
+
+			input := parse.NewInput(clean(a.Files[0].Data))
+			result, ok, err := scriptElement.Parse(input)
+			if err != nil {
+				t.Fatalf("parser error: %v", err)
+			}
+			if !ok {
+				t.Fatalf("failed to parse at %d", input.Index())
+			}
+
+			se, isScriptElement := result.(ScriptElement)
+			if !isScriptElement {
+				t.Fatalf("expected ScriptElement, got %T", result)
+			}
+			if len(se.Contents) != 1 {
+				t.Fatalf("expected 1 content, got %d", len(se.Contents))
+			}
+			expected := clean(a.Files[1].Data)
+			if diff := cmp.Diff(*se.Contents[0].Value, string(expected)); diff != "" {
+				t.Fatalf("%s:\n%s", file, diff)
+			}
+		})
+	}
+}
+
 func TestScriptElementParser(t *testing.T) {
 	tests := []struct {
 		name     string
@@ -198,3 +241,24 @@ but it's commented out */
 		})
 	}
 }
+
+func FuzzScriptParser(f *testing.F) {
+	files, _ := filepath.Glob("scriptparsertestdata/*.txt")
+	if len(files) == 0 {
+		f.Errorf("no test files found")
+	}
+	for _, file := range files {
+		a, err := txtar.ParseFile(file)
+		if err != nil {
+			f.Fatal(err)
+		}
+		if len(a.Files) != 2 {
+			f.Fatalf("expected 2 files, got %d", len(a.Files))
+		}
+		f.Add(clean(a.Files[0].Data))
+	}
+
+	f.Fuzz(func(t *testing.T, input string) {
+		_, _, _ = scriptElement.Parse(parse.NewInput(input))
+	})
+}
diff --git a/parser/v2/scriptparsertestdata/showsuccessmessage.txt b/parser/v2/scriptparsertestdata/showsuccessmessage.txt
new file mode 100644
index 0000000..a6f7c95
--- /dev/null
+++ b/parser/v2/scriptparsertestdata/showsuccessmessage.txt
@@ -0,0 +1,20 @@
+-- in --
+<script>
+  function showSuccessMessage(responseText) {
+			const formResponse = document.getElementById('form-response');
+			formResponse.innerHTML = `
+				<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
+					${responseText}
+				</div>`;
+		}
+</script>
+-- out --
+
+  function showSuccessMessage(responseText) {
+			const formResponse = document.getElementById('form-response');
+			formResponse.innerHTML = `
+				<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
+					${responseText}
+				</div>`;
+		}
+
diff --git a/parser/v2/testdata/fuzz/FuzzScriptParser/0667fe9c719c304f b/parser/v2/testdata/fuzz/FuzzScriptParser/0667fe9c719c304f
new file mode 100644
index 0000000..ac4af77
--- /dev/null
+++ b/parser/v2/testdata/fuzz/FuzzScriptParser/0667fe9c719c304f
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("<script>/*/00000\x17I020000000////00000\x17000")
diff --git a/parser/v2/testdata/fuzz/FuzzScriptParser/21c86d8a2781524b b/parser/v2/testdata/fuzz/FuzzScriptParser/21c86d8a2781524b
new file mode 100644
index 0000000..1d9b126
--- /dev/null
+++ b/parser/v2/testdata/fuzz/FuzzScriptParser/21c86d8a2781524b
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("<script>00000000000000000000////0000000000")
diff --git a/parser/v2/testdata/fuzz/FuzzScriptParser/43cd47dd50874af5 b/parser/v2/testdata/fuzz/FuzzScriptParser/43cd47dd50874af5
new file mode 100644
index 0000000..15bc6ce
--- /dev/null
+++ b/parser/v2/testdata/fuzz/FuzzScriptParser/43cd47dd50874af5
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("<script>\n  function showSuccessMessage(responseText) {\n\t\t\tconst formRespo</scriptcument.getElementById('form-response');\n\t\t\tformResponse.innerHTML = `\n\t\t\t\t<div class=\"bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded\">\n\t\t\t\t\t${responseText}\n\t\t\t\t</div>`;\n\t\t}\nnse = do>")