Repository: gohugoio/hugo (language: go)

Issue:
tpl/internal: Sync go_templates
Of specific interest, https://github.com/golang/go/issues/56490 was resolved with https://github.com/golang/go/commit/be7068fb0804f661515c678bee9224b90b32869a. This was merged after the release og Go 1.19.3.

Reference: <https://discourse.gohugo.io/t/unexpected-assignment/41153>




Discussion / hints:

Fix patch:
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index 9296b7bdd79..2f8516e8d79 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -17,7 +17,7 @@ import (
 )
 
 func main() {
-	// The current is built with 41a82aa9c3 text/template/parse: allow space after continue or break
+	// The current is built with be7068fb0804f661515c678bee9224b90b32869a text/template: correct assignment, not declaration, in range
 	fmt.Println("Forking ...")
 	defer fmt.Println("Done ...")
 
diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go
index 34c1f477f0e..278a89bd75d 100644
--- a/tpl/internal/go_templates/fmtsort/sort.go
+++ b/tpl/internal/go_templates/fmtsort/sort.go
@@ -36,19 +36,18 @@ func (o *SortedMap) Swap(i, j int) {
 //
 // The ordering rules are more general than with Go's < operator:
 //
-//  - when applicable, nil compares low
-//  - ints, floats, and strings order by <
-//  - NaN compares less than non-NaN floats
-//  - bool compares false before true
-//  - complex compares real, then imag
-//  - pointers compare by machine address
-//  - channel values compare by machine address
-//  - structs compare each field in turn
-//  - arrays compare each element in turn.
-//    Otherwise identical arrays compare by length.
-//  - interface values compare first by reflect.Type describing the concrete type
-//    and then by concrete value as described in the previous rules.
-//
+//   - when applicable, nil compares low
+//   - ints, floats, and strings order by <
+//   - NaN compares less than non-NaN floats
+//   - bool compares false before true
+//   - complex compares real, then imag
+//   - pointers compare by machine address
+//   - channel values compare by machine address
+//   - structs compare each field in turn
+//   - arrays compare each element in turn.
+//     Otherwise identical arrays compare by length.
+//   - interface values compare first by reflect.Type describing the concrete type
+//     and then by concrete value as described in the previous rules.
 func Sort(mapValue reflect.Value) *SortedMap {
 	if mapValue.Type().Kind() != reflect.Map {
 		return nil
diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go
index c28e08dce2f..146a95d035c 100644
--- a/tpl/internal/go_templates/htmltemplate/context.go
+++ b/tpl/internal/go_templates/htmltemplate/context.go
@@ -80,7 +80,9 @@ func (c context) mangle(templateName string) string {
 // HTML5 parsing algorithm because a single token production in the HTML
 // grammar may contain embedded actions in a template. For instance, the quoted
 // HTML attribute produced by
-//     <div title="Hello {{.World}}">
+//
+//	<div title="Hello {{.World}}">
+//
 // is a single token in HTML's grammar but in a template spans several nodes.
 type state uint8
 
diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go
index b6a1504f85d..8422b49214b 100644
--- a/tpl/internal/go_templates/htmltemplate/doc.go
+++ b/tpl/internal/go_templates/htmltemplate/doc.go
@@ -12,14 +12,14 @@ The documentation here focuses on the security features of the package.
 For information about how to program the templates themselves, see the
 documentation for text/template.
 
-Introduction
+# Introduction
 
 This package wraps package text/template so you can share its template API
 to parse and execute HTML templates safely.
 
-  tmpl, err := template.New("name").Parse(...)
-  // Error checking elided
-  err = tmpl.Execute(out, data)
+	tmpl, err := template.New("name").Parse(...)
+	// Error checking elided
+	err = tmpl.Execute(out, data)
 
 If successful, tmpl will now be injection-safe. Otherwise, err is an error
 defined in the docs for ErrorCode.
@@ -34,38 +34,37 @@ provided below.
 
 Example
 
-  import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-  ...
-  t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
-  err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
+	import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+	...
+	t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
+	err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
 
 produces
 
-  Hello, <script>alert('you have been pwned')</script>!
+	Hello, <script>alert('you have been pwned')</script>!
 
 but the contextual autoescaping in html/template
 
-  import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
-  ...
-  t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
-  err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
+	import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+	...
+	t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
+	err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
 
 produces safe, escaped HTML output
 
-  Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
+	Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
 
-
-Contexts
+# Contexts
 
 This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
 functions to each simple action pipeline, so given the excerpt
 
-  <a href="/search?q={{.}}">{{.}}</a>
+	<a href="/search?q={{.}}">{{.}}</a>
 
 At parse time each {{.}} is overwritten to add escaping functions as necessary.
 In this case it becomes
 
-  <a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
+	<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
 
 where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping
 functions.
@@ -73,117 +72,113 @@ functions.
 For these internal escaping functions, if an action pipeline evaluates to
 a nil interface value, it is treated as though it were an empty string.
 
-Namespaced and data- attributes
+# Namespaced and data- attributes
 
 Attributes with a namespace are treated as if they had no namespace.
 Given the excerpt
 
-  <a my:href="{{.}}"></a>
+	<a my:href="{{.}}"></a>
 
 At parse time the attribute will be treated as if it were just "href".
 So at parse time the template becomes:
 
-  <a my:href="{{. | urlescaper | attrescaper}}"></a>
+	<a my:href="{{. | urlescaper | attrescaper}}"></a>
 
 Similarly to attributes with namespaces, attributes with a "data-" prefix are
 treated as if they had no "data-" prefix. So given
 
-  <a data-href="{{.}}"></a>
+	<a data-href="{{.}}"></a>
 
 At parse time this becomes
 
-  <a data-href="{{. | urlescaper | attrescaper}}"></a>
+	<a data-href="{{. | urlescaper | attrescaper}}"></a>
 
 If an attribute has both a namespace and a "data-" prefix, only the namespace
 will be removed when determining the context. For example
 
-  <a my:data-href="{{.}}"></a>
+	<a my:data-href="{{.}}"></a>
 
 This is handled as if "my:data-href" was just "data-href" and not "href" as
 it would be if the "data-" prefix were to be ignored too. Thus at parse
 time this becomes just
 
-  <a my:data-href="{{. | attrescaper}}"></a>
+	<a my:data-href="{{. | attrescaper}}"></a>
 
 As a special case, attributes with the namespace "xmlns" are always treated
 as containing URLs. Given the excerpts
 
-  <a xmlns:title="{{.}}"></a>
-  <a xmlns:href="{{.}}"></a>
-  <a xmlns:onclick="{{.}}"></a>
+	<a xmlns:title="{{.}}"></a>
+	<a xmlns:href="{{.}}"></a>
+	<a xmlns:onclick="{{.}}"></a>
 
 At parse time they become:
 
-  <a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
-  <a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
-  <a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
+	<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
+	<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
+	<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
 
-Errors
+# Errors
 
 See the documentation of ErrorCode for details.
 
-
-A fuller picture
+# A fuller picture
 
 The rest of this package comment may be skipped on first reading; it includes
 details necessary to understand escaping contexts and error messages. Most users
 will not need to understand these details.
 
-
-Contexts
+# Contexts
 
 Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows
 how {{.}} appears when used in the context to the left.
 
-  Context                          {{.}} After
-  {{.}}                            O'Reilly: How are &lt;i&gt;you&lt;/i&gt;?
-  <a title='{{.}}'>                O&#39;Reilly: How are you?
-  <a href="/{{.}}">                O&#39;Reilly: How are %3ci%3eyou%3c/i%3e?
-  <a href="?q={{.}}">              O&#39;Reilly%3a%20How%20are%3ci%3e...%3f
-  <a onx='f("{{.}}")'>             O\x27Reilly: How are \x3ci\x3eyou...?
-  <a onx='f({{.}})'>               "O\x27Reilly: How are \x3ci\x3eyou...?"
-  <a onx='pattern = /{{.}}/;'>     O\x27Reilly: How are \x3ci\x3eyou...\x3f
+	Context                          {{.}} After
+	{{.}}                            O'Reilly: How are &lt;i&gt;you&lt;/i&gt;?
+	<a title='{{.}}'>                O&#39;Reilly: How are you?
+	<a href="/{{.}}">                O&#39;Reilly: How are %3ci%3eyou%3c/i%3e?
+	<a href="?q={{.}}">              O&#39;Reilly%3a%20How%20are%3ci%3e...%3f
+	<a onx='f("{{.}}")'>             O\x27Reilly: How are \x3ci\x3eyou...?
+	<a onx='f({{.}})'>               "O\x27Reilly: How are \x3ci\x3eyou...?"
+	<a onx='pattern = /{{.}}/;'>     O\x27Reilly: How are \x3ci\x3eyou...\x3f
 
 If used in an unsafe context, then the value might be filtered out:
 
-  Context                          {{.}} After
-  <a href="{{.}}">                 #ZgotmplZ
+	Context                          {{.}} After
+	<a href="{{.}}">                 #ZgotmplZ
 
 since "O'Reilly:" is not an allowed protocol like "http:".
 
-
 If {{.}} is the innocuous word, `left`, then it can appear more widely,
 
-  Context                              {{.}} After
-  {{.}}                                left
-  <a title='{{.}}'>                    left
-  <a href='{{.}}'>                     left
-  <a href='/{{.}}'>                    left
-  <a href='?dir={{.}}'>                left
-  <a style="border-{{.}}: 4px">        left
-  <a style="align: {{.}}">             left
-  <a style="background: '{{.}}'>       left
-  <a style="background: url('{{.}}')>  left
-  <style>p.{{.}} {color:red}</style>   left
+	Context                              {{.}} After
+	{{.}}                                left
+	<a title='{{.}}'>                    left
+	<a href='{{.}}'>                     left
+	<a href='/{{.}}'>                    left
+	<a href='?dir={{.}}'>                left
+	<a style="border-{{.}}: 4px">        left
+	<a style="align: {{.}}">             left
+	<a style="background: '{{.}}'>       left
+	<a style="background: url('{{.}}')>  left
+	<style>p.{{.}} {color:red}</style>   left
 
 Non-string values can be used in JavaScript contexts.
 If {{.}} is
 
-  struct{A,B string}{ "foo", "bar" }
+	struct{A,B string}{ "foo", "bar" }
 
 in the escaped template
 
-  <script>var pair = {{.}};</script>
+	<script>var pair = {{.}};</script>
 
 then the template output is
 
-  <script>var pair = {"A": "foo", "B": "bar"};</script>
+	<script>var pair = {"A": "foo", "B": "bar"};</script>
 
 See package json to understand how non-string content is marshaled for
 embedding in JavaScript contexts.
 
-
-Typed Strings
+# Typed Strings
 
 By default, this package assumes that all pipelines produce a plain text string.
 It adds escaping pipeline stages necessary to correctly and safely embed that
@@ -197,24 +192,23 @@ exempted from escaping.
 
 The template
 
-  Hello, {{.}}!
+	Hello, {{.}}!
 
 can be invoked with
 
-  tmpl.Execute(out, template.HTML(`<b>World</b>`))
+	tmpl.Execute(out, template.HTML(`<b>World</b>`))
 
 to produce
 
-  Hello, <b>World</b>!
+	Hello, <b>World</b>!
 
 instead of the
 
-  Hello, &lt;b&gt;World&lt;b&gt;!
+	Hello, &lt;b&gt;World&lt;b&gt;!
 
 that would have been produced if {{.}} was a regular string.
 
-
-Security Model
+# Security Model
 
 https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition defines "safe" as used by this package.
 
diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go
index 21c86a9efa8..916b41a82c5 100644
--- a/tpl/internal/go_templates/htmltemplate/error.go
+++ b/tpl/internal/go_templates/htmltemplate/error.go
@@ -33,14 +33,17 @@ type ErrorCode int
 //
 // Output: "ZgotmplZ"
 // Example:
-//   <img src="{{.X}}">
-//   where {{.X}} evaluates to `javascript:...`
+//
+//	<img src="{{.X}}">
+//	where {{.X}} evaluates to `javascript:...`
+//
 // Discussion:
-//   "ZgotmplZ" is a special value that indicates that unsafe content reached a
-//   CSS or URL context at runtime. The output of the example will be
-//     <img src="#ZgotmplZ">
-//   If the data comes from a trusted source, use content types to exempt it
-//   from filtering: URL(`javascript:...`).
+//
+//	"ZgotmplZ" is a special value that indicates that unsafe content reached a
+//	CSS or URL context at runtime. The output of the example will be
+//	  <img src="#ZgotmplZ">
+//	If the data comes from a trusted source, use content types to exempt it
+//	from filtering: URL(`javascript:...`).
 const (
 	// OK indicates the lack of an error.
 	OK ErrorCode = iota
diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go
index 488894416e8..3aac865ef1b 100644
--- a/tpl/internal/go_templates/htmltemplate/escape.go
+++ b/tpl/internal/go_templates/htmltemplate/escape.go
@@ -412,13 +412,19 @@ func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode {
 // nudge returns the context that would result from following empty string
 // transitions from the input context.
 // For example, parsing:
-//     `<a href=`
+//
+//	`<a href=`
+//
 // will end in context{stateBeforeValue, attrURL}, but parsing one extra rune:
-//     `<a href=x`
+//
+//	`<a href=x`
+//
 // will end in context{stateURL, delimSpaceOrTagEnd, ...}.
 // There are two transitions that happen when the 'x' is seen:
 // (1) Transition from a before-value state to a start-of-value state without
-//     consuming any character.
+//
+//	consuming any character.
+//
 // (2) Consume 'x' and transition past the first value character.
 // In this case, nudging produces the context after (1) happens.
 func nudge(c context) context {
diff --git a/tpl/internal/go_templates/htmltemplate/html.go b/tpl/internal/go_templates/htmltemplate/html.go
index 19bd0ccb204..bcca0b51a0e 100644
--- a/tpl/internal/go_templates/htmltemplate/html.go
+++ b/tpl/internal/go_templates/htmltemplate/html.go
@@ -84,10 +84,12 @@ var htmlNormReplacementTable = []string{
 // <script>(function () {
 // var a = [], d = document.getElementById("d"), i, c, s;
 // for (i = 0; i < 0x10000; ++i) {
-//   c = String.fromCharCode(i);
-//   d.innerHTML = "<span title=" + c + "lt" + c + "></span>"
-//   s = d.getElementsByTagName("SPAN")[0];
-//   if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); }
+//
+//	c = String.fromCharCode(i);
+//	d.innerHTML = "<span title=" + c + "lt" + c + "></span>"
+//	s = d.getElementsByTagName("SPAN")[0];
+//	if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); }
+//
 // }
 // document.write(a.join(", "));
 // })()</script>
@@ -174,7 +176,7 @@ func htmlReplacer(s string, replacementTable []string, badRunes bool) string {
 // stripTags takes a snippet of HTML and returns only the text content.
 // For example, `<b>&iexcl;Hi!</b> <script>...</script>` -> `&iexcl;Hi! `.
 func stripTags(html string) string {
-	var b bytes.Buffer
+	var b strings.Builder
 	s, c, i, allText := []byte(html), context{}, 0, true
 	// Using the transition funcs helps us avoid mangling
 	// `<div title="1>2">` or `I <3 Ponies!`.
diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go
index b4ccaa648ec..d7454f101d1 100644
--- a/tpl/internal/go_templates/htmltemplate/template.go
+++ b/tpl/internal/go_templates/htmltemplate/template.go
@@ -65,6 +65,7 @@ func (t *Template) Templates() []*Template {
 //
 // missingkey: Control the behavior during execution if a map is
 // indexed with a key that is not present in the map.
+//
 //	"missingkey=default" or "missingkey=invalid"
 //		The default behavior: Do nothing and continue execution.
 //		If printed, the result of the index operation is the string
@@ -73,7 +74,6 @@ func (t *Template) Templates() []*Template {
 //		The operation returns the zero value for the map type's element.
 //	"missingkey=error"
 //		Execution stops immediately with an error.
-//
 func (t *Template) Option(opt ...string) *Template {
 	t.text.Option(opt...)
 	return t
@@ -329,14 +329,7 @@ func (t *Template) Name() string {
 	return t.text.Name()
 }
 
-// FuncMap is the type of the map defining the mapping from names to
-// functions. Each function must have either a single return value, or two
-// return values of which the second has type error. In that case, if the
-// second (error) argument evaluates to non-nil during execution, execution
-// terminates and Execute returns that error. FuncMap has the same base type
-// as FuncMap in "text/template", copied here so clients need not import
-// "text/template".
-type FuncMap map[string]any
+type FuncMap = template.FuncMap
 
 // Funcs adds the elements of the argument map to the template's function map.
 // It must be called before the template is parsed.
@@ -369,6 +362,7 @@ func (t *Template) Lookup(name string) *Template {
 // Must is a helper that wraps a call to a function returning (*Template, error)
 // and panics if the error is non-nil. It is intended for use in variable initializations
 // such as
+//
 //	var t = template.Must(template.New("name").Parse("html"))
 func Must(t *Template, err error) *Template {
 	if err != nil {
diff --git a/tpl/internal/go_templates/htmltemplate/url.go b/tpl/internal/go_templates/htmltemplate/url.go
index 93905586a2f..7820561dc02 100644
--- a/tpl/internal/go_templates/htmltemplate/url.go
+++ b/tpl/internal/go_templates/htmltemplate/url.go
@@ -5,7 +5,6 @@
 package template
 
 import (
-	"bytes"
 	"fmt"
 	"strings"
 )
@@ -19,15 +18,15 @@ import (
 //
 // This filter conservatively assumes that all schemes other than the following
 // are unsafe:
-//    * http:   Navigates to a new website, and may open a new window or tab.
-//              These side effects can be reversed by navigating back to the
-//              previous website, or closing the window or tab. No irreversible
-//              changes will take place without further user interaction with
-//              the new website.
-//    * https:  Same as http.
-//    * mailto: Opens an email program and starts a new draft. This side effect
-//              is not irreversible until the user explicitly clicks send; it
-//              can be undone by closing the email program.
+//   - http:   Navigates to a new website, and may open a new window or tab.
+//     These side effects can be reversed by navigating back to the
+//     previous website, or closing the window or tab. No irreversible
+//     changes will take place without further user interaction with
+//     the new website.
+//   - https:  Same as http.
+//   - mailto: Opens an email program and starts a new draft. This side effect
+//     is not irreversible until the user explicitly clicks send; it
+//     can be undone by closing the email program.
 //
 // To allow URLs containing other schemes to bypass this filter, developers must
 // explicitly indicate that such a URL is expected and safe by encapsulating it
@@ -76,7 +75,7 @@ func urlProcessor(norm bool, args ...any) string {
 	if t == contentTypeURL {
 		norm = true
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	if processURLOnto(s, norm, &b) {
 		return b.String()
 	}
@@ -85,7 +84,7 @@ func urlProcessor(norm bool, args ...any) string {
 
 // processURLOnto appends a normalized URL corresponding to its input to b
 // and reports whether the appended content differs from s.
-func processURLOnto(s string, norm bool, b *bytes.Buffer) bool {
+func processURLOnto(s string, norm bool, b *strings.Builder) bool {
 	b.Grow(len(s) + 16)
 	written := 0
 	// The byte loop below assumes that all URLs use UTF-8 as the
@@ -149,7 +148,7 @@ func srcsetFilterAndEscaper(args ...any) string {
 	case contentTypeURL:
 		// Normalizing gets rid of all HTML whitespace
 		// which separate the image URL from its metadata.
-		var b bytes.Buffer
+		var b strings.Builder
 		if processURLOnto(s, true, &b) {
 			s = b.String()
 		}
@@ -157,7 +156,7 @@ func srcsetFilterAndEscaper(args ...any) string {
 		return strings.ReplaceAll(s, ",", "%2c")
 	}
 
-	var b bytes.Buffer
+	var b strings.Builder
 	written := 0
 	for i := 0; i < len(s); i++ {
 		if s[i] == ',' {
@@ -183,7 +182,7 @@ func isHTMLSpaceOrASCIIAlnum(c byte) bool {
 	return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
 }
 
-func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) {
+func filterSrcsetElement(s string, left int, right int, b *strings.Builder) {
 	start := left
 	for start < right && isHTMLSpace(s[start]) {
 		start++
diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go
index 10093881fbe..7817a17b96e 100644
--- a/tpl/internal/go_templates/texttemplate/doc.go
+++ b/tpl/internal/go_templates/texttemplate/doc.go
@@ -18,7 +18,6 @@ structure as execution proceeds.
 The input text for a template is UTF-8-encoded text in any format.
 "Actions"--data evaluations or control structures--are delimited by
 "{{" and "}}"; all text outside actions is copied to the output unchanged.
-Except for raw strings, actions may not span newlines, although comments can.
 
 Once parsed, a template may be executed safely in parallel, although if parallel
 executions share a Writer the output may be interleaved.
@@ -425,10 +424,10 @@ The syntax of such definitions is to surround each template declaration with a
 The define action names the template being created by providing a string
 constant. Here is a simple example:
 
-	`{{define "T1"}}ONE{{end}}
+	{{define "T1"}}ONE{{end}}
 	{{define "T2"}}TWO{{end}}
 	{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
-	{{template "T3"}}`
+	{{template "T3"}}
 
 This defines two templates, T1 and T2, and a third T3 that invokes the other two
 when it is executed. Finally it invokes T3. If executed this template will
diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
index 4460771cba6..62b2e519ea0 100644
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -94,6 +94,12 @@ type missingValType struct{}
 
 var missingVal = reflect.ValueOf(missingValType{})
 
+var missingValReflectType = reflect.TypeOf(missingValType{})
+
+func isMissing(v reflect.Value) bool {
+	return v.IsValid() && v.Type() == missingValReflectType
+}
+
 // at marks the state to be on node n, for error reporting.
 func (s *state) at(node parse.Node) {
 	s.node = node
@@ -357,11 +363,19 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
 	oneIteration := func(index, elem reflect.Value) {
 		// Set top var (lexically the second if there are two) to the element.
 		if len(r.Pipe.Decl) > 0 {
-			s.setTopVar(1, elem)
+			if r.Pipe.IsAssign {
+				s.setVar(r.Pipe.Decl[0].Ident[0], elem)
+			} else {
+				s.setTopVar(1, elem)
+			}
 		}
 		// Set next var (lexically the first if there are two) to the index.
 		if len(r.Pipe.Decl) > 1 {
-			s.setTopVar(2, index)
+			if r.Pipe.IsAssign {
+				s.setVar(r.Pipe.Decl[1].Ident[0], index)
+			} else {
+				s.setTopVar(2, index)
+			}
 		}
 		defer s.pop(mark)
 		defer func() {
@@ -471,7 +485,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
 }
 
 func (s *state) notAFunction(args []parse.Node, final reflect.Value) {
-	if len(args) > 1 || final != missingVal {
+	if len(args) > 1 || !isMissing(final) {
 		s.errorf("can't give argument to non-function %s", args[0])
 	}
 }
@@ -629,7 +643,7 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
 	if method := ptr.MethodByName(fieldName); method.IsValid() {
 		return s.evalCall(dot, method, false, node, fieldName, args, final)
 	}
-	hasArgs := len(args) > 1 || final != missingVal
+	hasArgs := len(args) > 1 || !isMissing(final)
 	// It's not a method; must be a field of a struct or an element of a map.
 	switch receiver.Kind() {
 	case reflect.Struct:
@@ -700,7 +714,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
 	}
 	typ := fun.Type()
 	numIn := len(args)
-	if final != missingVal {
+	if !isMissing(final) {
 		numIn++
 	}
 	numFixed := len(args)
@@ -763,7 +777,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
 		}
 	}
 	// Add final value if necessary.
-	if final != missingVal {
+	if !isMissing(final) {
 		t := typ.In(typ.NumIn() - 1)
 		if typ.IsVariadic() {
 			if numIn-1 < numFixed {
diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go
index dca5ed28db8..dbea6e705a0 100644
--- a/tpl/internal/go_templates/texttemplate/funcs.go
+++ b/tpl/internal/go_templates/texttemplate/funcs.go
@@ -5,7 +5,6 @@
 package template
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
 	"io"
@@ -436,14 +435,33 @@ func basicKind(v reflect.Value) (kind, error) {
 	return invalidKind, errBadComparisonType
 }
 
+// isNil returns true if v is the zero reflect.Value, or nil of its type.
+func isNil(v reflect.Value) bool {
+	if !v.IsValid() {
+		return true
+	}
+	switch v.Kind() {
+	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
+		return v.IsNil()
+	}
+	return false
+}
+
+// canCompare reports whether v1 and v2 are both the same kind, or one is nil.
+// Called only when dealing with nillable types, or there's about to be an error.
+func canCompare(v1, v2 reflect.Value) bool {
+	k1 := v1.Kind()
+	k2 := v2.Kind()
+	if k1 == k2 {
+		return true
+	}
+	// We know the type can be compared to nil.
+	return k1 == reflect.Invalid || k2 == reflect.Invalid
+}
+
 // eq evaluates the comparison a == b || a == c || ...
 func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
 	arg1 = indirectInterface(arg1)
-	if arg1 != zero {
-		if t1 := arg1.Type(); !t1.Comparable() {
-			return false, fmt.Errorf("uncomparable type %s: %v", t1, arg1)
-		}
-	}
 	if len(arg2) == 0 {
 		return false, errNoComparison
 	}
@@ -479,11 +497,14 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
 			case uintKind:
 				truth = arg1.Uint() == arg.Uint()
 			default:
-				if arg == zero || arg1 == zero {
-					truth = arg1 == arg
+				if !canCompare(arg1, arg) {
+					return false, fmt.Errorf("non-comparable types %s: %v, %s: %v", arg1, arg1.Type(), arg.Type(), arg)
+				}
+				if isNil(arg1) || isNil(arg) {
+					truth = isNil(arg) == isNil(arg1)
 				} else {
-					if t2 := arg.Type(); !t2.Comparable() {
-						return false, fmt.Errorf("uncomparable type %s: %v", t2, arg)
+					if !arg.Type().Comparable() {
+						return false, fmt.Errorf("non-comparable type %s: %v", arg, arg.Type())
 					}
 					truth = arg1.Interface() == arg.Interface()
 				}
@@ -620,7 +641,7 @@ func HTMLEscapeString(s string) string {
 	if !strings.ContainsAny(s, "'\"&<>\000") {
 		return s
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	HTMLEscape(&b, []byte(s))
 	return b.String()
 }
@@ -703,7 +724,7 @@ func JSEscapeString(s string) string {
 	if strings.IndexFunc(s, jsIsSpecial) < 0 {
 		return s
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	JSEscape(&b, []byte(s))
 	return b.String()
 }
@@ -729,7 +750,9 @@ func URLQueryEscaper(args ...any) string {
 }
 
 // evalArgs formats the list of arguments into a string. It is therefore equivalent to
+//
 //	fmt.Sprint(args...)
+//
 // except that each argument is indirected (if a pointer), as required,
 // using the same rules as the default string evaluation during template
 // execution.
diff --git a/tpl/internal/go_templates/texttemplate/helper.go b/tpl/internal/go_templates/texttemplate/helper.go
index 57905e613a4..48af3928b39 100644
--- a/tpl/internal/go_templates/texttemplate/helper.go
+++ b/tpl/internal/go_templates/texttemplate/helper.go
@@ -19,6 +19,7 @@ import (
 // Must is a helper that wraps a call to a function returning (*Template, error)
 // and panics if the error is non-nil. It is intended for use in variable
 // initializations such as
+//
 //	var t = template.Must(template.New("name").Parse("text"))
 func Must(t *Template, err error) *Template {
 	if err != nil {
diff --git a/tpl/internal/go_templates/texttemplate/option.go b/tpl/internal/go_templates/texttemplate/option.go
index 1035afad72a..ea2fd80c069 100644
--- a/tpl/internal/go_templates/texttemplate/option.go
+++ b/tpl/internal/go_templates/texttemplate/option.go
@@ -30,6 +30,7 @@ type option struct {
 //
 // missingkey: Control the behavior during execution if a map is
 // indexed with a key that is not present in the map.
+//
 //	"missingkey=default" or "missingkey=invalid"
 //		The default behavior: Do nothing and continue execution.
 //		If printed, the result of the index operation is the string
@@ -38,7 +39,6 @@ type option struct {
 //		The operation returns the zero value for the map type's element.
 //	"missingkey=error"
 //		Execution stops immediately with an error.
-//
 func (t *Template) Option(opt ...string) *Template {
 	t.init()
 	for _, s := range opt {
diff --git a/tpl/internal/go_templates/texttemplate/parse/lex.go b/tpl/internal/go_templates/texttemplate/parse/lex.go
index 40d0411121b..3e60a1ecef5 100644
--- a/tpl/internal/go_templates/texttemplate/parse/lex.go
+++ b/tpl/internal/go_templates/texttemplate/parse/lex.go
@@ -111,31 +111,36 @@ type stateFn func(*lexer) stateFn
 
 // lexer holds the state of the scanner.
 type lexer struct {
-	name        string    // the name of the input; used only for error reports
-	input       string    // the string being scanned
-	leftDelim   string    // start of action
-	rightDelim  string    // end of action
-	emitComment bool      // emit itemComment tokens.
-	pos         Pos       // current position in the input
-	start       Pos       // start position of this item
-	width       Pos       // width of last rune read from input
-	items       chan item // channel of scanned items
-	parenDepth  int       // nesting depth of ( ) exprs
-	line        int       // 1+number of newlines seen
-	startLine   int       // start line of this item
-	breakOK     bool      // break keyword allowed
-	continueOK  bool      // continue keyword allowed
+	name         string // the name of the input; used only for error reports
+	input        string // the string being scanned
+	leftDelim    string // start of action marker
+	rightDelim   string // end of action marker
+	pos          Pos    // current position in the input
+	start        Pos    // start position of this item
+	atEOF        bool   // we have hit the end of input and returned eof
+	parenDepth   int    // nesting depth of ( ) exprs
+	line         int    // 1+number of newlines seen
+	startLine    int    // start line of this item
+	item         item   // item to return to parser
+	insideAction bool   // are we inside an action?
+	options      lexOptions
+}
+
+// lexOptions control behavior of the lexer. All default to false.
+type lexOptions struct {
+	emitComment bool // emit itemComment tokens.
+	breakOK     bool // break keyword allowed
+	continueOK  bool // continue keyword allowed
 }
 
 // next returns the next rune in the input.
 func (l *lexer) next() rune {
 	if int(l.pos) >= len(l.input) {
-		l.width = 0
+		l.atEOF = true
 		return eof
 	}
 	r, w := utf8.DecodeRuneInString(l.input[l.pos:])
-	l.width = Pos(w)
-	l.pos += l.width
+	l.pos += Pos(w)
 	if r == '\n' {
 		l.line++
 	}
@@ -149,23 +154,41 @@ func (l *lexer) peek() rune {
 	return r
 }
 
-// backup steps back one rune. Can only be called once per call of next.
+// backup steps back one rune.
 func (l *lexer) backup() {
-	l.pos -= l.width
-	// Correct newline count.
-	if l.width == 1 && l.input[l.pos] == '\n' {
-		l.line--
+	if !l.atEOF && l.pos > 0 {
+		r, w := utf8.DecodeLastRuneInString(l.input[:l.pos])
+		l.pos -= Pos(w)
+		// Correct newline count.
+		if r == '\n' {
+			l.line--
+		}
 	}
 }
 
-// emit passes an item back to the client.
-func (l *lexer) emit(t itemType) {
-	l.items <- item{t, l.start, l.input[l.start:l.pos], l.startLine}
+// thisItem returns the item at the current input point with the specified type
+// and advances the input.
+func (l *lexer) thisItem(t itemType) item {
+	i := item{t, l.start, l.input[l.start:l.pos], l.startLine}
 	l.start = l.pos
 	l.startLine = l.line
+	return i
+}
+
+// emit passes the trailing text as an item back to the parser.
+func (l *lexer) emit(t itemType) stateFn {
+	return l.emitItem(l.thisItem(t))
+}
+
+// emitItem passes the specified item to the parser.
+func (l *lexer) emitItem(i item) stateFn {
+	l.item = i
+	return nil
 }
 
 // ignore skips over the pending input before this point.
+// It tracks newlines in the ignored text, so use it only
+// for text that is skipped without calling l.next.
 func (l *lexer) ignore() {
 	l.line += strings.Count(l.input[l.start:l.pos], "\n")
 	l.start = l.pos
@@ -191,25 +214,31 @@ func (l *lexer) acceptRun(valid string) {
 // errorf returns an error token and terminates the scan by passing
 // back a nil pointer that will be the next state, terminating l.nextItem.
 func (l *lexer) errorf(format string, args ...any) stateFn {
-	l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine}
+	l.item = item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine}
+	l.start = 0
+	l.pos = 0
+	l.input = l.input[:0]
 	return nil
 }
 
 // nextItem returns the next item from the input.
 // Called by the parser, not in the lexing goroutine.
 func (l *lexer) nextItem() item {
-	return <-l.items
-}
-
-// drain drains the output so the lexing goroutine will exit.
-// Called by the parser, not in the lexing goroutine.
-func (l *lexer) drain() {
-	for range l.items {
+	l.item = item{itemEOF, l.pos, "EOF", l.startLine}
+	state := lexText
+	if l.insideAction {
+		state = lexInsideAction
+	}
+	for {
+		state = state(l)
+		if state == nil {
+			return l.item
+		}
 	}
 }
 
 // lex creates a new scanner for the input string.
-func lex(name, input, left, right string, emitComment bool) *lexer {
+func lex(name, input, left, right string) *lexer {
 	if left == "" {
 		left = leftDelim
 	}
@@ -217,25 +246,15 @@ func lex(name, input, left, right string, emitComment bool) *lexer {
 		right = rightDelim
 	}
 	l := &lexer{
-		name:        name,
-		input:       input,
-		leftDelim:   left,
-		rightDelim:  right,
-		emitComment: emitComment,
-		items:       make(chan item),
-		line:        1,
-		startLine:   1,
-	}
-	go l.run()
-	return l
-}
-
-// run runs the state machine for the lexer.
-func (l *lexer) run() {
-	for state := lexText; state != nil; {
-		state = state(l)
+		name:         name,
+		input:        input,
+		leftDelim:    left,
+		rightDelim:   right,
+		line:         1,
+		startLine:    1,
+		insideAction: false,
 	}
-	close(l.items)
+	return l
 }
 
 // state functions
@@ -249,31 +268,33 @@ const (
 
 // lexText scans until an opening action delimiter, "{{".
 func lexText(l *lexer) stateFn {
-	l.width = 0
 	if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
-		ldn := Pos(len(l.leftDelim))
-		l.pos += Pos(x)
-		trimLength := Pos(0)
-		if hasLeftTrimMarker(l.input[l.pos+ldn:]) {
-			trimLength = rightTrimLength(l.input[l.start:l.pos])
-		}
-		l.pos -= trimLength
-		if l.pos > l.start {
+		if x > 0 {
+			l.pos += Pos(x)
+			// Do we trim any trailing space?
+			trimLength := Pos(0)
+			delimEnd := l.pos + Pos(len(l.leftDelim))
+			if hasLeftTrimMarker(l.input[delimEnd:]) {
+				trimLength = rightTrimLength(l.input[l.start:l.pos])
+			}
+			l.pos -= trimLength
 			l.line += strings.Count(l.input[l.start:l.pos], "\n")
-			l.emit(itemText)
+			i := l.thisItem(itemText)
+			l.pos += trimLength
+			l.ignore()
+			if len(i.val) > 0 {
+				return l.emitItem(i)
+			}
 		}
-		l.pos += trimLength
-		l.ignore()
 		return lexLeftDelim
 	}
 	l.pos = Pos(len(l.input))
 	// Correctly reached EOF.
 	if l.pos > l.start {
 		l.line += strings.Count(l.input[l.start:l.pos], "\n")
-		l.emit(itemText)
+		return l.emit(itemText)
 	}
-	l.emit(itemEOF)
-	return nil
+	return l.emit(itemEOF)
 }
 
 // rightTrimLength returns the length of the spaces at the end of the string.
@@ -298,6 +319,7 @@ func leftTrimLength(s string) Pos {
 }
 
 // lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker.
+// (The text to be trimmed has already been emitted.)
 func lexLeftDelim(l *lexer) stateFn {
 	l.pos += Pos(len(l.leftDelim))
 	trimSpace := hasLeftTrimMarker(l.input[l.pos:])
@@ -310,28 +332,27 @@ func lexLeftDelim(l *lexer) stateFn {
 		l.ignore()
 		return lexComment
 	}
-	l.emit(itemLeftDelim)
+	i := l.thisItem(itemLeftDelim)
+	l.insideAction = true
 	l.pos += afterMarker
 	l.ignore()
 	l.parenDepth = 0
-	return lexInsideAction
+	return l.emitItem(i)
 }
 
 // lexComment scans a comment. The left comment marker is known to be present.
 func lexComment(l *lexer) stateFn {
 	l.pos += Pos(len(leftComment))
-	i := strings.Index(l.input[l.pos:], rightComment)
-	if i < 0 {
+	x := strings.Index(l.input[l.pos:], rightComment)
+	if x < 0 {
 		return l.errorf("unclosed comment")
 	}
-	l.pos += Pos(i + len(rightComment))
+	l.pos += Pos(x + len(rightComment))
 	delim, trimSpace := l.atRightDelim()
 	if !delim {
 		return l.errorf("comment ends before closing delimiter")
 	}
-	if l.emitComment {
-		l.emit(itemComment)
-	}
+	i := l.thisItem(itemComment)
 	if trimSpace {
 		l.pos += trimMarkerLen
 	}
@@ -340,23 +361,27 @@ func lexComment(l *lexer) stateFn {
 		l.pos += leftTrimLength(l.input[l.pos:])
 	}
 	l.ignore()
+	if l.options.emitComment {
+		return l.emitItem(i)
+	}
 	return lexText
 }
 
 // lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker.
 func lexRightDelim(l *lexer) stateFn {
-	trimSpace := hasRightTrimMarker(l.input[l.pos:])
+	_, trimSpace := l.atRightDelim()
 	if trimSpace {
 		l.pos += trimMarkerLen
 		l.ignore()
 	}
 	l.pos += Pos(len(l.rightDelim))
-	l.emit(itemRightDelim)
+	i := l.thisItem(itemRightDelim)
 	if trimSpace {
 		l.pos += leftTrimLength(l.input[l.pos:])
 		l.ignore()
 	}
-	return lexText
+	l.insideAction = false
+	return l.emitItem(i)
 }
 
 // lexInsideAction scans the elements inside action delimiters.
@@ -378,14 +403,14 @@ func lexInsideAction(l *lexer) stateFn {
 		l.backup() // Put space back in case we have " -}}".
 		return lexSpace
 	case r == '=':
-		l.emit(itemAssign)
+		return l.emit(itemAssign)
 	case r == ':':
 		if l.next() != '=' {
 			return l.errorf("expected :=")
 		}
-		l.emit(itemDeclare)
+		return l.emit(itemDeclare)
 	case r == '|':
-		l.emit(itemPipe)
+		return l.emit(itemPipe)
 	case r == '"':
 		return lexQuote
 	case r == '`':
@@ -410,20 +435,19 @@ func lexInsideAction(l *lexer) stateFn {
 		l.backup()
 		return lexIdentifier
 	case r == '(':
-		l.emit(itemLeftParen)
 		l.parenDepth++
+		return l.emit(itemLeftParen)
 	case r == ')':
-		l.emit(itemRightParen)
 		l.parenDepth--
 		if l.parenDepth < 0 {
-			return l.errorf("unexpected right paren %#U", r)
+			return l.errorf("unexpected right paren")
 		}
+		return l.emit(itemRightParen)
 	case r <= unicode.MaxASCII && unicode.IsPrint(r):
-		l.emit(itemChar)
+		return l.emit(itemChar)
 	default:
 		return l.errorf("unrecognized character in action: %#U", r)
 	}
-	return lexInsideAction
 }
 
 // lexSpace scans a run of space characters.
@@ -448,13 +472,11 @@ func lexSpace(l *lexer) stateFn {
 			return lexRightDelim // On the delim, so go right to that.
 		}
 	}
-	l.emit(itemSpace)
-	return lexInsideAction
+	return l.emit(itemSpace)
 }
 
 // lexIdentifier scans an alphanumeric.
 func lexIdentifier(l *lexer) stateFn {
-Loop:
 	for {
 		switch r := l.next(); {
 		case isAlphaNumeric(r):
@@ -468,22 +490,19 @@ Loop:
 			switch {
 			case key[word] > itemKeyword:
 				item := key[word]
-				if item == itemBreak && !l.breakOK || item == itemContinue && !l.continueOK {
-					l.emit(itemIdentifier)
-				} else {
-					l.emit(item)
+				if item == itemBreak && !l.options.breakOK || item == itemContinue && !l.options.continueOK {
+					return l.emit(itemIdentifier)
 				}
+				return l.emit(item)
 			case word[0] == '.':
-				l.emit(itemField)
+				return l.emit(itemField)
 			case word == "true", word == "false":
-				l.emit(itemBool)
+				return l.emit(itemBool)
 			default:
-				l.emit(itemIdentifier)
+				return l.emit(itemIdentifier)
 			}
-			break Loop
 		}
 	}
-	return lexInsideAction
 }
 
 // lexField scans a field: .Alphanumeric.
@@ -496,8 +515,7 @@ func lexField(l *lexer) stateFn {
 // The $ has been scanned.
 func lexVariable(l *lexer) stateFn {
 	if l.atTerminator() { // Nothing interesting follows -> "$".
-		l.emit(itemVariable)
-		return lexInsideAction
+		return l.emit(itemVariable)
 	}
 	return lexFieldOrVariable(l, itemVariable)
 }
@@ -507,11 +525,9 @@ func lexVariable(l *lexer) stateFn {
 func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
 	if l.atTerminator() { // Nothing interesting follows -> "." or "$".
 		if typ == itemVariable {
-			l.emit(itemVariable)
-		} else {
-			l.emit(itemDot)
+			return l.emit(itemVariable)
 		}
-		return lexInsideAction
+		return l.emit(itemDot)
 	}
 	var r rune
 	for {
@@ -524,8 +540,7 @@ func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
 	if !l.atTerminator() {
 		return l.errorf("bad character %#U", r)
 	}
-	l.emit(typ)
-	return lexInsideAction
+	return l.emit(typ)
 }
 
 // atTerminator reports whether the input is at valid termination character to
@@ -541,13 +556,7 @@ func (l *lexer) atTerminator() bool {
 	case eof, '.', ',', '|', ':', ')', '(':
 		return true
 	}
-	// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
-	// succeed but should fail) but only in extremely rare cases caused by willfully
-	// bad choice of delimiter.
-	if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r {
-		return true
-	}
-	return false
+	return strings.HasPrefix(l.input[l.pos:], l.rightDelim)
 }
 
 // lexChar scans a character constant. The initial quote is already
@@ -567,8 +576,7 @@ Loop:
 			break Loop
 		}
 	}
-	l.emit(itemCharConstant)
-	return lexInsideAction
+	return l.emit(itemCharConstant)
 }
 
 // lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
@@ -584,11 +592,9 @@ func lexNumber(l *lexer) stateFn {
 		if !l.scanNumber() || l.input[l.pos-1] != 'i' {
 			return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
 		}
-		l.emit(itemComplex)
-	} else {
-		l.emit(itemNumber)
+		return l.emit(itemComplex)
 	}
-	return lexInsideAction
+	return l.emit(itemNumber)
 }
 
 func (l *lexer) scanNumber() bool {
@@ -644,8 +650,7 @@ Loop:
 			break Loop
 		}
 	}
-	l.emit(itemString)
-	return lexInsideAction
+	return l.emit(itemString)
 }
 
 // lexRawQuote scans a raw quoted string.
@@ -659,8 +664,7 @@ Loop:
 			break Loop
 		}
 	}
-	l.emit(itemRawString)
-	return lexInsideAction
+	return l.emit(itemRawString)
 }
 
 // isSpace reports whether r is a space character.
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
index ce548b08865..87b7618f757 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse.go
@@ -210,7 +210,6 @@ func (t *Tree) recover(errp *error) {
 			panic(e)
 		}
 		if t != nil {
-			t.lex.drain()
 			t.stopParse()
 		}
 		*errp = e.(error)
@@ -224,8 +223,6 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
 	t.vars = []string{"$"}
 	t.funcs = funcs
 	t.treeSet = treeSet
-	lex.breakOK = !t.hasFunction("break")
-	lex.continueOK = !t.hasFunction("continue")
 }
 
 // stopParse terminates parsing.
@@ -243,8 +240,13 @@ func (t *Tree) stopParse() {
 func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) {
 	defer t.recover(&err)
 	t.ParseName = t.Name
-	emitComment := t.Mode&ParseComments != 0
-	t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim, emitComment), treeSet)
+	lexer := lex(t.Name, text, leftDelim, rightDelim)
+	lexer.options = lexOptions{
+		emitComment: t.Mode&ParseComments != 0,
+		breakOK:     !t.hasFunction("break"),
+		continueOK:  !t.hasFunction("continue"),
+	}
+	t.startParse(funcs, lexer, treeSet)
 	t.text = text
 	t.parse()
 	t.add()
@@ -341,7 +343,9 @@ func (t *Tree) parseDefinition() {
 }
 
 // itemList:
+//
 //	textOrAction*
+//
 // Terminates at {{end}} or {{else}}, returned separately.
 func (t *Tree) itemList() (list *ListNode, next Node) {
 	list = t.newList(t.peekNonSpace().pos)
@@ -358,6 +362,7 @@ func (t *Tree) itemList() (list *ListNode, next Node) {
 }
 
 // textOrAction:
+//
 //	text | comment | action
 func (t *Tree) textOrAction() Node {
 	switch token := t.nextNonSpace(); token.typ {
@@ -380,8 +385,10 @@ func (t *Tree) clearActionLine() {
 }
 
 // Action:
+//
 //	control
 //	command ("|" command)*
+//
 // Left delim is past. Now get actions.
 // First word could be a keyword such as range.
 func (t *Tree) action() (n Node) {
@@ -412,7 +419,9 @@ func (t *Tree) action() (n Node) {
 }
 
 // Break:
+//
 //	{{break}}
+//
 // Break keyword is past.
 func (t *Tree) breakControl(pos Pos, line int) Node {
 	if token := t.nextNonSpace(); token.typ != itemRightDelim {
@@ -425,7 +434,9 @@ func (t *Tree) breakControl(pos Pos, line int) Node {
 }
 
 // Continue:
+//
 //	{{continue}}
+//
 // Continue keyword is past.
 func (t *Tree) continueControl(pos Pos, line int) Node {
 	if token := t.nextNonSpace(); token.typ != itemRightDelim {
@@ -438,6 +449,7 @@ func (t *Tree) continueControl(pos Pos, line int) Node {
 }
 
 // Pipeline:
+//
 //	declarations? command ('|' command)*
 func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
 	token := t.peekNonSpace()
@@ -549,16 +561,20 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
 }
 
 // If:
+//
 //	{{if pipeline}} itemList {{end}}
 //	{{if pipeline}} itemList {{else}} itemList {{end}}
+//
 // If keyword is past.
 func (t *Tree) ifControl() Node {
 	return t.newIf(t.parseControl(true, "if"))
 }
 
 // Range:
+//
 //	{{range pipeline}} itemList {{end}}
 //	{{range pipeline}} itemList {{else}} itemList {{end}}
+//
 // Range keyword is past.
 func (t *Tree) rangeControl() Node {
 	r := t.newRange(t.parseControl(false, "range"))
@@ -566,22 +582,28 @@ func (t *Tree) rangeControl() Node {
 }
 
 // With:
+//
 //	{{with pipeline}} itemList {{end}}
 //	{{with pipeline}} itemList {{else}} itemList {{end}}
+//
 // If keyword is past.
 func (t *Tree) withControl() Node {
 	return t.newWith(t.parseControl(false, "with"))
 }
 
 // End:
+//
 //	{{end}}
+//
 // End keyword is past.
 func (t *Tree) endControl() Node {
 	return t.newEnd(t.expect(itemRightDelim, "end").pos)
 }
 
 // Else:
+//
 //	{{else}}
+//
 // Else keyword is past.
 func (t *Tree) elseControl() Node {
 	// Special case for "else if".
@@ -595,7 +617,9 @@ func (t *Tree) elseControl() Node {
 }
 
 // Block:
+//
 //	{{block stringValue pipeline}}
+//
 // Block keyword is past.
 // The name must be something that can evaluate to a string.
 // The pipeline is mandatory.
@@ -623,7 +647,9 @@ func (t *Tree) blockControl() Node {
 }
 
 // Template:
+//
 //	{{template stringValue pipeline}}
+//
 // Template keyword is past. The name must be something that can evaluate
 // to a string.
 func (t *Tree) templateControl() Node {
@@ -654,7 +680,9 @@ func (t *Tree) parseTemplateName(token item, context string) (name string) {
 }
 
 // command:
+//
 //	operand (space operand)*
+//
 // space-separated arguments up to a pipeline character or right delimiter.
 // we consume the pipe character but leave the right delim to terminate the action.
 func (t *Tree) command() *CommandNode {
@@ -684,7 +712,9 @@ func (t *Tree) command() *CommandNode {
 }
 
 // operand:
+//
 //	term .Field*
+//
 // An operand is a space-separated component of a command,
 // a term possibly followed by field accesses.
 // A nil return means the next item is not an operand.
@@ -718,12 +748,14 @@ func (t *Tree) operand() Node {
 }
 
 // term:
+//
 //	literal (number, string, nil, boolean)
 //	function (identifier)
 //	.
 //	.Field
 //	$
 //	'(' pipeline ')'
+//
 // A term is a simple "expression".
 // A nil return means the next item is not a term.
 func (t *Tree) term() Node {


Test patch:
diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go
index a05e8a3c3df..064b0910730 100644
--- a/tpl/internal/go_templates/fmtsort/sort_test.go
+++ b/tpl/internal/go_templates/fmtsort/sort_test.go
@@ -147,7 +147,7 @@ func sprint(data any) string {
 		}
 		b.WriteString(sprintKey(key))
 		b.WriteRune(':')
-		b.WriteString(fmt.Sprint(om.Value[i]))
+		fmt.Fprint(b, om.Value[i])
 	}
 	return b.String()
 }
diff --git a/tpl/internal/go_templates/htmltemplate/clone_test.go b/tpl/internal/go_templates/htmltemplate/clone_test.go
index 553f656b53c..7db335b5b7d 100644
--- a/tpl/internal/go_templates/htmltemplate/clone_test.go
+++ b/tpl/internal/go_templates/htmltemplate/clone_test.go
@@ -8,7 +8,6 @@
 package template
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
 	"io"
@@ -26,7 +25,7 @@ func TestAddParseTreeHTML(t *testing.T) {
 		t.Fatal(err)
 	}
 	added := Must(root.AddParseTree("b", tree["b"]))
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 	err = added.ExecuteTemplate(b, "a", "1>0")
 	if err != nil {
 		t.Fatal(err)
@@ -43,7 +42,7 @@ func TestClone(t *testing.T) {
 	// In the t2 template, it will be in a JavaScript context.
 	// In the t3 template, it will be in a CSS context.
 	const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}`
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 
 	// Create an incomplete template t0.
 	t0 := Must(New("t0").Parse(tmpl))
diff --git a/tpl/internal/go_templates/htmltemplate/content_test.go b/tpl/internal/go_templates/htmltemplate/content_test.go
index 29221a4ade0..fac4774cc84 100644
--- a/tpl/internal/go_templates/htmltemplate/content_test.go
+++ b/tpl/internal/go_templates/htmltemplate/content_test.go
@@ -284,7 +284,7 @@ func TestTypedContent(t *testing.T) {
 			[]string{
 				`#ZgotmplZ`,
 				`#ZgotmplZ`,
-				// Commas are not esacped
+				// Commas are not escaped.
 				`Hello,#ZgotmplZ`,
 				// Leading spaces are not percent escapes.
 				` dir=%22ltr%22`,
@@ -389,7 +389,7 @@ func TestTypedContent(t *testing.T) {
 		tmpl := Must(New("x").Parse(test.input))
 		pre := strings.Index(test.input, "{{.}}")
 		post := len(test.input) - (pre + 5)
-		var b bytes.Buffer
+		var b strings.Builder
 		for i, x := range data {
 			b.Reset()
 			if err := tmpl.Execute(&b, x); err != nil {
@@ -423,7 +423,7 @@ func (s *errorer) Error() string {
 
 func TestStringer(t *testing.T) {
 	s := &myStringer{3}
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 	tmpl := Must(New("x").Parse("{{.}}"))
 	if err := tmpl.Execute(b, s); err != nil {
 		t.Fatal(err)
diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go
index adf160b5d15..a08ea57ef70 100644
--- a/tpl/internal/go_templates/htmltemplate/escape_test.go
+++ b/tpl/internal/go_templates/htmltemplate/escape_test.go
@@ -693,7 +693,7 @@ func TestEscape(t *testing.T) {
 			t.Errorf("%s: tree not set properly", test.name)
 			continue
 		}
-		b := new(bytes.Buffer)
+		b := new(strings.Builder)
 		if err := tmpl.Execute(b, data); err != nil {
 			t.Errorf("%s: template execution failed: %s", test.name, err)
 			continue
@@ -740,7 +740,7 @@ func TestEscapeMap(t *testing.T) {
 		},
 	} {
 		tmpl := Must(New("").Parse(test.input))
-		b := new(bytes.Buffer)
+		b := new(strings.Builder)
 		if err := tmpl.Execute(b, data); err != nil {
 			t.Errorf("%s: template execution failed: %s", test.desc, err)
 			continue
@@ -882,7 +882,7 @@ func TestEscapeSet(t *testing.T) {
 			t.Errorf("error parsing %q: %v", source, err)
 			continue
 		}
-		var b bytes.Buffer
+		var b strings.Builder
 
 		if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil {
 			t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main"))
@@ -1833,7 +1833,7 @@ func TestIndirectPrint(t *testing.T) {
 	bp := &b
 	bpp := &bp
 	tmpl := Must(New("t").Parse(`{{.}}`))
-	var buf bytes.Buffer
+	var buf strings.Builder
 	err := tmpl.Execute(&buf, ap)
 	if err != nil {
 		t.Errorf("Unexpected error: %s", err)
@@ -1876,7 +1876,7 @@ func TestPipeToMethodIsEscaped(t *testing.T) {
 				t.Errorf("panicked: %v\n", panicValue)
 			}
 		}()
-		var b bytes.Buffer
+		var b strings.Builder
 		tmpl.Execute(&b, Issue7379(0))
 		return b.String()
 	}
@@ -1909,7 +1909,7 @@ func TestIdempotentExecute(t *testing.T) {
 		Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`))
 	Must(tmpl.
 		Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`))
-	got := new(bytes.Buffer)
+	got := new(strings.Builder)
 	var err error
 	// Ensure that "hello" produces the same output when executed twice.
 	want := "Hello, Ladies &amp; Gentlemen!"
@@ -1952,7 +1952,7 @@ func TestOrphanedTemplate(t *testing.T) {
 	t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`))
 	t2 := Must(t1.New("foo").Parse(`bar`))
 
-	var b bytes.Buffer
+	var b strings.Builder
 	const wantError = `template: "foo" is an incomplete or empty template`
 	if err := t1.Execute(&b, "javascript:alert(1)"); err == nil {
 		t.Fatal("expected error executing t1")
@@ -1981,7 +1981,7 @@ func TestAliasedParseTreeDoesNotOverescape(t *testing.T) {
 	if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil {
 		t.Fatalf("AddParseTree error: %v", err)
 	}
-	var b1, b2 bytes.Buffer
+	var b1, b2 strings.Builder
 	if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil {
 		t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err)
 	}
diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go
index 08195af0ec6..0f29cb06051 100644
--- a/tpl/internal/go_templates/htmltemplate/exec_test.go
+++ b/tpl/internal/go_templates/htmltemplate/exec_test.go
@@ -769,7 +769,7 @@ func mapOfThree() any {
 }
 
 func testExecute(execTests []execTest, template *Template, t *testing.T) {
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 	funcs := FuncMap{
 		"add":         add,
 		"count":       count,
@@ -861,7 +861,7 @@ func TestDelims(t *testing.T) {
 		if err != nil {
 			t.Fatalf("delim %q text %q parse err %s", left, text, err)
 		}
-		var b = new(bytes.Buffer)
+		var b = new(strings.Builder)
 		err = tmpl.Execute(b, value)
 		if err != nil {
 			t.Fatalf("delim %q exec err %s", left, err)
@@ -1002,7 +1002,7 @@ func TestTree(t *testing.T) {
 	if err != nil {
 		t.Fatal("parse error:", err)
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
 	// First by looking up the template.
 	err = tmpl.Lookup("tree").Execute(&b, tree)
@@ -1196,33 +1196,39 @@ var cmpTests = []cmpTest{
 	{"eq .Iface1 .Iface1", "true", true},
 	{"eq .Iface1 .Iface2", "false", true},
 	{"eq .Iface2 .Iface2", "true", true},
+	{"eq .Map .Map", "true", true},        // Uncomparable types but nil is OK.
+	{"eq .Map nil", "true", true},         // Uncomparable types but nil is OK.
+	{"eq nil .Map", "true", true},         // Uncomparable types but nil is OK.
+	{"eq .Map .NonNilMap", "false", true}, // Uncomparable types but nil is OK.
 	// Errors
-	{"eq `xy` 1", "", false},       // Different types.
-	{"eq 2 2.0", "", false},        // Different types.
-	{"lt true true", "", false},    // Unordered types.
-	{"lt 1+0i 1+0i", "", false},    // Unordered types.
-	{"eq .Ptr 1", "", false},       // Incompatible types.
-	{"eq .Ptr .NegOne", "", false}, // Incompatible types.
-	{"eq .Map .Map", "", false},    // Uncomparable types.
-	{"eq .Map .V1", "", false},     // Uncomparable types.
+	{"eq `xy` 1", "", false},                // Different types.
+	{"eq 2 2.0", "", false},                 // Different types.
+	{"lt true true", "", false},             // Unordered types.
+	{"lt 1+0i 1+0i", "", false},             // Unordered types.
+	{"eq .Ptr 1", "", false},                // Incompatible types.
+	{"eq .Ptr .NegOne", "", false},          // Incompatible types.
+	{"eq .Map .V1", "", false},              // Uncomparable types.
+	{"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types.
 }
 
 func TestComparison(t *testing.T) {
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 	var cmpStruct = struct {
 		Uthree, Ufour  uint
 		NegOne, Three  int
 		Ptr, NilPtr    *int
+		NonNilMap      map[int]int
 		Map            map[int]int
 		V1, V2         V
 		Iface1, Iface2 fmt.Stringer
 	}{
-		Uthree: 3,
-		Ufour:  4,
-		NegOne: -1,
-		Three:  3,
-		Ptr:    new(int),
-		Iface1: b,
+		Uthree:    3,
+		Ufour:     4,
+		NegOne:    -1,
+		Three:     3,
+		Ptr:       new(int),
+		NonNilMap: make(map[int]int),
+		Iface1:    b,
 	}
 	for _, test := range cmpTests {
 		text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
@@ -1254,7 +1260,7 @@ func TestMissingMapKey(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	// By default, just get "<no value>" // NOTE: not in html/template, get empty string
 	err = tmpl.Execute(&b, data)
 	if err != nil {
@@ -1423,7 +1429,7 @@ func TestBlock(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	var buf bytes.Buffer
+	var buf strings.Builder
 	if err := tmpl.Execute(&buf, "hello"); err != nil {
 		t.Fatal(err)
 	}
@@ -1529,7 +1535,7 @@ func TestAddrOfIndex(t *testing.T) {
 	}
 	for _, text := range texts {
 		tmpl := Must(New("tmpl").Parse(text))
-		var buf bytes.Buffer
+		var buf strings.Builder
 		err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}}))
 		if err != nil {
 			t.Fatalf("%s: Execute: %v", text, err)
@@ -1585,7 +1591,7 @@ func TestInterfaceValues(t *testing.T) {
 
 	for _, tt := range tests {
 		tmpl := Must(New("tmpl").Parse(tt.text))
-		var buf bytes.Buffer
+		var buf strings.Builder
 		err := tmpl.Execute(&buf, map[string]any{
 			"PlusOne": func(n int) int {
 				return n + 1
@@ -1680,7 +1686,7 @@ func TestIssue31810(t *testing.T) {
 	t.Skip("broken in html/template")
 
 	// A simple value with no arguments is fine.
-	var b bytes.Buffer
+	var b strings.Builder
 	const text = "{{ (.)  }}"
 	tmpl, err := New("").Parse(text)
 	if err != nil {
diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go
index 92073b37a22..483a3694f5d 100644
--- a/tpl/internal/go_templates/htmltemplate/js_test.go
+++ b/tpl/internal/go_templates/htmltemplate/js_test.go
@@ -8,7 +8,6 @@
 package template
 
 import (
-	"bytes"
 	"math"
 	"strings"
 	"testing"
@@ -324,7 +323,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
 
 		// Escape it rune by rune to make sure that any
 		// fast-path checking does not break escaping.
-		var buf bytes.Buffer
+		var buf strings.Builder
 		for _, c := range input {
 			buf.WriteString(test.escaper(string(c)))
 		}
diff --git a/tpl/internal/go_templates/htmltemplate/multi_test.go b/tpl/internal/go_templates/htmltemplate/multi_test.go
index 14cd7c766eb..f3e629b8864 100644
--- a/tpl/internal/go_templates/htmltemplate/multi_test.go
+++ b/tpl/internal/go_templates/htmltemplate/multi_test.go
@@ -11,8 +11,8 @@ package template
 
 import (
 	"archive/zip"
-	"bytes"
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@@ -249,7 +249,7 @@ func TestEmptyTemplate(t *testing.T) {
 				t.Fatal(err)
 			}
 		}
-		buf := &bytes.Buffer{}
+		buf := &strings.Builder{}
 		if err := m.Execute(buf, c.in); err != nil {
 			t.Error(i, err)
 			continue
@@ -284,7 +284,7 @@ func TestIssue19294(t *testing.T) {
 				t.Fatal(err)
 			}
 		}
-		var buf bytes.Buffer
+		var buf strings.Builder
 		res.Execute(&buf, 0)
 		if buf.String() != "stylesheet" {
 			t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")
diff --git a/tpl/internal/go_templates/htmltemplate/template_test.go b/tpl/internal/go_templates/htmltemplate/template_test.go
index 8a8f2f38c14..0838308a146 100644
--- a/tpl/internal/go_templates/htmltemplate/template_test.go
+++ b/tpl/internal/go_templates/htmltemplate/template_test.go
@@ -30,7 +30,7 @@ func TestTemplateClone(t *testing.T) {
 
 	const want = "stuff"
 	parsed := Must(clone.Parse(want))
-	var buf bytes.Buffer
+	var buf strings.Builder
 	err = parsed.Execute(&buf, nil)
 	if err != nil {
 		t.Fatal(err)
@@ -211,7 +211,7 @@ func (c *testCase) mustNotParse(t *Template, text string) {
 }
 
 func (c *testCase) mustExecute(t *Template, val any, want string) {
-	var buf bytes.Buffer
+	var buf strings.Builder
 	err := t.Execute(&buf, val)
 	if err != nil {
 		c.t.Fatalf("execute: %v", err)
diff --git a/tpl/internal/go_templates/testenv/exec.go b/tpl/internal/go_templates/testenv/exec.go
new file mode 100644
index 00000000000..cd06c4f7e1a
--- /dev/null
+++ b/tpl/internal/go_templates/testenv/exec.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package testenv
+
+import (
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+	"sync"
+	"testing"
+)
+
+// HasExec reports whether the current system can start new processes
+// using os.StartProcess or (more commonly) exec.Command.
+func HasExec() bool {
+	switch runtime.GOOS {
+	case "js", "ios":
+		return false
+	}
+	return true
+}
+
+// MustHaveExec checks that the current system can start new processes
+// using os.StartProcess or (more commonly) exec.Command.
+// If not, MustHaveExec calls t.Skip with an explanation.
+func MustHaveExec(t testing.TB) {
+	if !HasExec() {
+		t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
+	}
+}
+
+var execPaths sync.Map // path -> error
+
+// MustHaveExecPath checks that the current system can start the named executable
+// using os.StartProcess or (more commonly) exec.Command.
+// If not, MustHaveExecPath calls t.Skip with an explanation.
+func MustHaveExecPath(t testing.TB, path string) {
+	MustHaveExec(t)
+
+	err, found := execPaths.Load(path)
+	if !found {
+		_, err = exec.LookPath(path)
+		err, _ = execPaths.LoadOrStore(path, err)
+	}
+	if err != nil {
+		t.Skipf("skipping test: %s: %s", path, err)
+	}
+}
+
+// CleanCmdEnv will fill cmd.Env with the environment, excluding certain
+// variables that could modify the behavior of the Go tools such as
+// GODEBUG and GOTRACEBACK.
+func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
+	if cmd.Env != nil {
+		panic("environment already set")
+	}
+	for _, env := range os.Environ() {
+		// Exclude GODEBUG from the environment to prevent its output
+		// from breaking tests that are trying to parse other command output.
+		if strings.HasPrefix(env, "GODEBUG=") {
+			continue
+		}
+		// Exclude GOTRACEBACK for the same reason.
+		if strings.HasPrefix(env, "GOTRACEBACK=") {
+			continue
+		}
+		cmd.Env = append(cmd.Env, env)
+	}
+	return cmd
+}
diff --git a/tpl/internal/go_templates/testenv/noopt.go b/tpl/internal/go_templates/testenv/noopt.go
new file mode 100644
index 00000000000..ae2a3d011a0
--- /dev/null
+++ b/tpl/internal/go_templates/testenv/noopt.go
@@ -0,0 +1,12 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build noopt
+
+package testenv
+
+// OptimizationOff reports whether optimization is disabled.
+func OptimizationOff() bool {
+	return true
+}
diff --git a/tpl/internal/go_templates/testenv/opt.go b/tpl/internal/go_templates/testenv/opt.go
new file mode 100644
index 00000000000..1bb96f73a12
--- /dev/null
+++ b/tpl/internal/go_templates/testenv/opt.go
@@ -0,0 +1,12 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !noopt
+
+package testenv
+
+// OptimizationOff reports whether optimization is disabled.
+func OptimizationOff() bool {
+	return false
+}
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
index 510b5406e80..4e38f5f0409 100644
--- a/tpl/internal/go_templates/testenv/testenv.go
+++ b/tpl/internal/go_templates/testenv/testenv.go
@@ -11,10 +11,12 @@
 package testenv
 
 import (
-	"bytes"
 	"errors"
 	"flag"
+	"fmt"
+
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
+
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -23,7 +25,6 @@ import (
 	"strings"
 	"sync"
 	"testing"
-	"time"
 )
 
 // Builder reports the name of the builder running this test
@@ -34,7 +35,7 @@ func Builder() string {
 	return os.Getenv("GO_BUILDER_NAME")
 }
 
-// HasGoBuild reports whether the current system can build programs with ``go build''
+// HasGoBuild reports whether the current system can build programs with “go build”
 // and then run them with os.StartProcess or exec.Command.
 func HasGoBuild() bool {
 	if os.Getenv("GO_GCFLAGS") != "" {
@@ -51,7 +52,7 @@ func HasGoBuild() bool {
 	return true
 }
 
-// MustHaveGoBuild checks that the current system can build programs with ``go build''
+// MustHaveGoBuild checks that the current system can build programs with “go build”
 // and then run them with os.StartProcess or exec.Command.
 // If not, MustHaveGoBuild calls t.Skip with an explanation.
 func MustHaveGoBuild(t testing.TB) {
@@ -63,13 +64,13 @@ func MustHaveGoBuild(t testing.TB) {
 	}
 }
 
-// HasGoRun reports whether the current system can run programs with ``go run.''
+// HasGoRun reports whether the current system can run programs with “go run.”
 func HasGoRun() bool {
 	// For now, having go run and having go build are the same.
 	return HasGoBuild()
 }
 
-// MustHaveGoRun checks that the current system can run programs with ``go run.''
+// MustHaveGoRun checks that the current system can run programs with “go run.”
 // If not, MustHaveGoRun calls t.Skip with an explanation.
 func MustHaveGoRun(t testing.TB) {
 	if !HasGoRun() {
@@ -96,6 +97,100 @@ func GoToolPath(t testing.TB) string {
 	return path
 }
 
+var (
+	gorootOnce sync.Once
+	gorootPath string
+	gorootErr  error
+)
+
+func findGOROOT() (string, error) {
+	gorootOnce.Do(func() {
+		gorootPath = runtime.GOROOT()
+		if gorootPath != "" {
+			// If runtime.GOROOT() is non-empty, assume that it is valid.
+			//
+			// (It might not be: for example, the user may have explicitly set GOROOT
+			// to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
+			// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
+			// rare, and if that happens the user can fix what they broke.)
+			return
+		}
+
+		// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
+		// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
+		// without GOROOT and the tree hasn't been moved there yet).
+		//
+		// Since this is internal/testenv, we can cheat and assume that the caller
+		// is a test of some package in a subdirectory of GOROOT/src. ('go test'
+		// runs the test in the directory containing the packaged under test.) That
+		// means that if we start walking up the tree, we should eventually find
+		// GOROOT/src/go.mod, and we can report the parent directory of that.
+
+		cwd, err := os.Getwd()
+		if err != nil {
+			gorootErr = fmt.Errorf("finding GOROOT: %w", err)
+			return
+		}
+
+		dir := cwd
+		for {
+			parent := filepath.Dir(dir)
+			if parent == dir {
+				// dir is either "." or only a volume name.
+				gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
+				return
+			}
+
+			if base := filepath.Base(dir); base != "src" {
+				dir = parent
+				continue // dir cannot be GOROOT/src if it doesn't end in "src".
+			}
+
+			b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
+			if err != nil {
+				if os.IsNotExist(err) {
+					dir = parent
+					continue
+				}
+				gorootErr = fmt.Errorf("finding GOROOT: %w", err)
+				return
+			}
+			goMod := string(b)
+
+			for goMod != "" {
+				var line string
+				line, goMod, _ = strings.Cut(goMod, "\n")
+				fields := strings.Fields(line)
+				if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
+					// Found "module std", which is the module declaration in GOROOT/src!
+					gorootPath = parent
+					return
+				}
+			}
+		}
+	})
+
+	return gorootPath, gorootErr
+}
+
+// GOROOT reports the path to the directory containing the root of the Go
+// project source tree. This is normally equivalent to runtime.GOROOT, but
+// works even if the test binary was built with -trimpath.
+//
+// If GOROOT cannot be found, GOROOT skips t if t is non-nil,
+// or panics otherwise.
+func GOROOT(t testing.TB) string {
+	path, err := findGOROOT()
+	if err != nil {
+		if t == nil {
+			panic(err)
+		}
+		t.Helper()
+		t.Skip(err)
+	}
+	return path
+}
+
 // GoTool reports the path to the Go tool.
 func GoTool() (string, error) {
 	if !HasGoBuild() {
@@ -105,7 +200,11 @@ func GoTool() (string, error) {
 	if runtime.GOOS == "windows" {
 		exeSuffix = ".exe"
 	}
-	path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix)
+	goroot, err := findGOROOT()
+	if err != nil {
+		return "", fmt.Errorf("cannot find go tool: %w", err)
+	}
+	path := filepath.Join(goroot, "bin", "go"+exeSuffix)
 	if _, err := os.Stat(path); err == nil {
 		return path, nil
 	}
@@ -116,16 +215,6 @@ func GoTool() (string, error) {
 	return goBin, nil
 }
 
-// HasExec reports whether the current system can start new processes
-// using os.StartProcess or (more commonly) exec.Command.
-func HasExec() bool {
-	switch runtime.GOOS {
-	case "js", "ios":
-		return false
-	}
-	return true
-}
-
 // HasSrc reports whether the entire source tree is available under GOROOT.
 func HasSrc() bool {
 	switch runtime.GOOS {
@@ -135,33 +224,6 @@ func HasSrc() bool {
 	return true
 }
 
-// MustHaveExec checks that the current system can start new processes
-// using os.StartProcess or (more commonly) exec.Command.
-// If not, MustHaveExec calls t.Skip with an explanation.
-func MustHaveExec(t testing.TB) {
-	if !HasExec() {
-		t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
-	}
-}
-
-var execPaths sync.Map // path -> error
-
-// MustHaveExecPath checks that the current system can start the named executable
-// using os.StartProcess or (more commonly) exec.Command.
-// If not, MustHaveExecPath calls t.Skip with an explanation.
-func MustHaveExecPath(t testing.TB, path string) {
-	MustHaveExec(t)
-
-	err, found := execPaths.Load(path)
-	if !found {
-		_, err = exec.LookPath(path)
-		err, _ = execPaths.LoadOrStore(path, err)
-	}
-	if err != nil {
-		t.Skipf("skipping test: %s: %s", path, err)
-	}
-}
-
 // HasExternalNetwork reports whether the current system can use
 // external (non-localhost) networks.
 func HasExternalNetwork() bool {
@@ -194,32 +256,6 @@ func MustHaveCGO(t testing.TB) {
 	}
 }
 
-// CanInternalLink reports whether the current system can link programs with
-// internal linking.
-// (This is the opposite of cmd/internal/sys.MustLinkExternal. Keep them in sync.)
-func CanInternalLink() bool {
-	switch runtime.GOOS {
-	case "android":
-		if runtime.GOARCH != "arm64" {
-			return false
-		}
-	case "ios":
-		if runtime.GOARCH == "arm64" {
-			return false
-		}
-	}
-	return true
-}
-
-// MustInternalLink checks that the current system can link programs with internal
-// linking.
-// If not, MustInternalLink calls t.Skip with an explanation.
-func MustInternalLink(t testing.TB) {
-	if !CanInternalLink() {
-		t.Skipf("skipping test: internal linking on %s/%s is not supported", runtime.GOOS, runtime.GOARCH)
-	}
-}
-
 // HasSymlink reports whether the current system can use os.Symlink.
 func HasSymlink() bool {
 	ok, _ := hasSymlink()
@@ -267,28 +303,6 @@ func SkipFlakyNet(t testing.TB) {
 	}
 }
 
-// CleanCmdEnv will fill cmd.Env with the environment, excluding certain
-// variables that could modify the behavior of the Go tools such as
-// GODEBUG and GOTRACEBACK.
-func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
-	if cmd.Env != nil {
-		panic("environment already set")
-	}
-	for _, env := range os.Environ() {
-		// Exclude GODEBUG from the environment to prevent its output
-		// from breaking tests that are trying to parse other command output.
-		if strings.HasPrefix(env, "GODEBUG=") {
-			continue
-		}
-		// Exclude GOTRACEBACK for the same reason.
-		if strings.HasPrefix(env, "GOTRACEBACK=") {
-			continue
-		}
-		cmd.Env = append(cmd.Env, env)
-	}
-	return cmd
-}
-
 // CPUIsSlow reports whether the CPU running the test is suspected to be slow.
 func CPUIsSlow() bool {
 	switch runtime.GOARCH {
@@ -309,58 +323,10 @@ func SkipIfShortAndSlow(t testing.TB) {
 	}
 }
 
-// RunWithTimeout runs cmd and returns its combined output. If the
-// subprocess exits with a non-zero status, it will log that status
-// and return a non-nil error, but this is not considered fatal.
-func RunWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) {
-	args := cmd.Args
-	if args == nil {
-		args = []string{cmd.Path}
-	}
-
-	var b bytes.Buffer
-	cmd.Stdout = &b
-	cmd.Stderr = &b
-	if err := cmd.Start(); err != nil {
-		t.Fatalf("starting %s: %v", args, err)
-	}
-
-	// If the process doesn't complete within 1 minute,
-	// assume it is hanging and kill it to get a stack trace.
-	p := cmd.Process
-	done := make(chan bool)
-	go func() {
-		scale := 1
-		// This GOARCH/GOOS test is copied from cmd/dist/test.go.
-		// TODO(iant): Have cmd/dist update the environment variable.
-		if runtime.GOARCH == "arm" || runtime.GOOS == "windows" {
-			scale = 2
-		}
-		if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
-			if sc, err := strconv.Atoi(s); err == nil {
-				scale = sc
-			}
-		}
-
-		select {
-		case <-done:
-		case <-time.After(time.Duration(scale) * time.Minute):
-			p.Signal(Sigquit)
-			// If SIGQUIT doesn't do it after a little
-			// while, kill the process.
-			select {
-			case <-done:
-			case <-time.After(time.Duration(scale) * 30 * time.Second):
-				p.Signal(os.Kill)
-			}
-		}
-	}()
-
-	err := cmd.Wait()
-	if err != nil {
-		t.Logf("%s exit status: %v", args, err)
+// SkipIfOptimizationOff skips t if optimization is disabled.
+func SkipIfOptimizationOff(t testing.TB) {
+	if OptimizationOff() {
+		t.Helper()
+		t.Skip("skipping test with optimization disabled")
 	}
-	close(done)
-
-	return b.Bytes(), err
 }
diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go
new file mode 100644
index 00000000000..97c92a6e949
--- /dev/null
+++ b/tpl/internal/go_templates/testenv/testenv_test.go
@@ -0,0 +1,54 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package testenv_test
+
+import (
+	"os"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
+)
+
+func _TestGoToolLocation(t *testing.T) {
+	testenv.MustHaveGoBuild(t)
+
+	var exeSuffix string
+	if runtime.GOOS == "windows" {
+		exeSuffix = ".exe"
+	}
+
+	// Tests are defined to run within their package source directory,
+	// and this package's source directory is $GOROOT/src/internal/testenv.
+	// The 'go' command is installed at $GOROOT/bin/go, so if the environment
+	// is correct then testenv.GoTool() should be identical to ../../../bin/go.
+
+	relWant := "../../../bin/go" + exeSuffix
+	absWant, err := filepath.Abs(relWant)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	wantInfo, err := os.Stat(absWant)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("found go tool at %q (%q)", relWant, absWant)
+
+	goTool, err := testenv.GoTool()
+	if err != nil {
+		t.Fatalf("testenv.GoTool(): %v", err)
+	}
+	t.Logf("testenv.GoTool() = %q", goTool)
+
+	gotInfo, err := os.Stat(goTool)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !os.SameFile(wantInfo, gotInfo) {
+		t.Fatalf("%q is not the same file as %q", absWant, goTool)
+	}
+}
diff --git a/tpl/internal/go_templates/testenv/testenv_unix.go b/tpl/internal/go_templates/testenv/testenv_unix.go
index 3dc5daf45e7..a97e88da2f6 100644
--- a/tpl/internal/go_templates/testenv/testenv_unix.go
+++ b/tpl/internal/go_templates/testenv/testenv_unix.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+//go:build unix
 
 package testenv
 
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index 64cb87ec6b3..45edb9e9bb2 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -695,6 +695,8 @@ var execTests = []execTest{
 	{"bug18a", "{{eq . '.'}}", "true", '.', true},
 	{"bug18b", "{{eq . 'e'}}", "true", 'e', true},
 	{"bug18c", "{{eq . 'P'}}", "true", 'P', true},
+
+	{"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true},
 }
 
 func zeroArgs() string {
@@ -775,7 +777,7 @@ func mapOfThree() any {
 }
 
 func testExecute(execTests []execTest, template *Template, t *testing.T) {
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 	funcs := FuncMap{
 		"add":         add,
 		"count":       count,
@@ -864,7 +866,7 @@ func TestDelims(t *testing.T) {
 		if err != nil {
 			t.Fatalf("delim %q text %q parse err %s", left, text, err)
 		}
-		var b = new(bytes.Buffer)
+		var b = new(strings.Builder)
 		err = tmpl.Execute(b, value)
 		if err != nil {
 			t.Fatalf("delim %q exec err %s", left, err)
@@ -1027,7 +1029,7 @@ func TestTree(t *testing.T) {
 	if err != nil {
 		t.Fatal("parse error:", err)
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
 	// First by looking up the template.
 	err = tmpl.Lookup("tree").Execute(&b, tree)
@@ -1223,33 +1225,39 @@ var cmpTests = []cmpTest{
 	{"eq .NilIface .Iface1", "false", true},
 	{"eq .NilIface 0", "false", true},
 	{"eq 0 .NilIface", "false", true},
+	{"eq .Map .Map", "true", true},        // Uncomparable types but nil is OK.
+	{"eq .Map nil", "true", true},         // Uncomparable types but nil is OK.
+	{"eq nil .Map", "true", true},         // Uncomparable types but nil is OK.
+	{"eq .Map .NonNilMap", "false", true}, // Uncomparable types but nil is OK.
 	// Errors
-	{"eq `xy` 1", "", false},       // Different types.
-	{"eq 2 2.0", "", false},        // Different types.
-	{"lt true true", "", false},    // Unordered types.
-	{"lt 1+0i 1+0i", "", false},    // Unordered types.
-	{"eq .Ptr 1", "", false},       // Incompatible types.
-	{"eq .Ptr .NegOne", "", false}, // Incompatible types.
-	{"eq .Map .Map", "", false},    // Uncomparable types.
-	{"eq .Map .V1", "", false},     // Uncomparable types.
+	{"eq `xy` 1", "", false},                // Different types.
+	{"eq 2 2.0", "", false},                 // Different types.
+	{"lt true true", "", false},             // Unordered types.
+	{"lt 1+0i 1+0i", "", false},             // Unordered types.
+	{"eq .Ptr 1", "", false},                // Incompatible types.
+	{"eq .Ptr .NegOne", "", false},          // Incompatible types.
+	{"eq .Map .V1", "", false},              // Uncomparable types.
+	{"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types.
 }
 
 func TestComparison(t *testing.T) {
-	b := new(bytes.Buffer)
+	b := new(strings.Builder)
 	var cmpStruct = struct {
 		Uthree, Ufour    uint
 		NegOne, Three    int
 		Ptr, NilPtr      *int
+		NonNilMap        map[int]int
 		Map              map[int]int
 		V1, V2           V
 		Iface1, NilIface fmt.Stringer
 	}{
-		Uthree: 3,
-		Ufour:  4,
-		NegOne: -1,
-		Three:  3,
-		Ptr:    new(int),
-		Iface1: b,
+		Uthree:    3,
+		Ufour:     4,
+		NegOne:    -1,
+		Three:     3,
+		Ptr:       new(int),
+		NonNilMap: make(map[int]int),
+		Iface1:    b,
 	}
 	for _, test := range cmpTests {
 		text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
@@ -1281,7 +1289,7 @@ func TestMissingMapKey(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	var b bytes.Buffer
+	var b strings.Builder
 	// By default, just get "<no value>"
 	err = tmpl.Execute(&b, data)
 	if err != nil {
@@ -1451,7 +1459,7 @@ func TestBlock(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	var buf bytes.Buffer
+	var buf strings.Builder
 	if err := tmpl.Execute(&buf, "hello"); err != nil {
 		t.Fatal(err)
 	}
@@ -1557,7 +1565,7 @@ func TestAddrOfIndex(t *testing.T) {
 	}
 	for _, text := range texts {
 		tmpl := Must(New("tmpl").Parse(text))
-		var buf bytes.Buffer
+		var buf strings.Builder
 		err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}}))
 		if err != nil {
 			t.Fatalf("%s: Execute: %v", text, err)
@@ -1613,7 +1621,7 @@ func TestInterfaceValues(t *testing.T) {
 
 	for _, tt := range tests {
 		tmpl := Must(New("tmpl").Parse(tt.text))
-		var buf bytes.Buffer
+		var buf strings.Builder
 		err := tmpl.Execute(&buf, map[string]any{
 			"PlusOne": func(n int) int {
 				return n + 1
@@ -1706,7 +1714,7 @@ func TestExecutePanicDuringCall(t *testing.T) {
 // Issue 31810. Check that a parenthesized first argument behaves properly.
 func TestIssue31810(t *testing.T) {
 	// A simple value with no arguments is fine.
-	var b bytes.Buffer
+	var b strings.Builder
 	const text = "{{ (.)  }}"
 	tmpl, err := New("").Parse(text)
 	if err != nil {
diff --git a/tpl/internal/go_templates/texttemplate/multi_test.go b/tpl/internal/go_templates/texttemplate/multi_test.go
index e3c9ec3ae7a..6e383c131f8 100644
--- a/tpl/internal/go_templates/texttemplate/multi_test.go
+++ b/tpl/internal/go_templates/texttemplate/multi_test.go
@@ -10,10 +10,10 @@ package template
 // Tests for multiple-template parsing and execution.
 
 import (
-	"bytes"
 	"fmt"
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 	"os"
+	"strings"
 	"testing"
 )
 
@@ -245,7 +245,7 @@ func TestClone(t *testing.T) {
 		}
 	}
 	// Execute root.
-	var b bytes.Buffer
+	var b strings.Builder
 	err = root.ExecuteTemplate(&b, "a", 0)
 	if err != nil {
 		t.Fatal(err)
@@ -284,7 +284,7 @@ func TestAddParseTree(t *testing.T) {
 		t.Fatal(err)
 	}
 	// Execute.
-	var b bytes.Buffer
+	var b strings.Builder
 	err = added.ExecuteTemplate(&b, "a", 0)
 	if err != nil {
 		t.Fatal(err)
@@ -413,7 +413,7 @@ func TestEmptyTemplate(t *testing.T) {
 				t.Fatal(err)
 			}
 		}
-		buf := &bytes.Buffer{}
+		buf := &strings.Builder{}
 		if err := m.Execute(buf, c.in); err != nil {
 			t.Error(i, err)
 			continue
@@ -448,7 +448,7 @@ func TestIssue19294(t *testing.T) {
 				t.Fatal(err)
 			}
 		}
-		var buf bytes.Buffer
+		var buf strings.Builder
 		res.Execute(&buf, 0)
 		if buf.String() != "stylesheet" {
 			t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")
diff --git a/tpl/internal/go_templates/texttemplate/parse/lex_test.go b/tpl/internal/go_templates/texttemplate/parse/lex_test.go
index 9189035fe64..11e88792e3e 100644
--- a/tpl/internal/go_templates/texttemplate/parse/lex_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/lex_test.go
@@ -362,8 +362,7 @@ var lexTests = []lexTest{
 	{"extra right paren", "{{3)}}", []item{
 		tLeft,
 		mkItem(itemNumber, "3"),
-		tRpar,
-		mkItem(itemError, `unexpected right paren U+0029 ')'`),
+		mkItem(itemError, "unexpected right paren"),
 	}},
 
 	// Fixed bugs
@@ -397,7 +396,12 @@ var lexTests = []lexTest{
 
 // collect gathers the emitted items into a slice.
 func collect(t *lexTest, left, right string) (items []item) {
-	l := lex(t.name, t.input, left, right, true)
+	l := lex(t.name, t.input, left, right)
+	l.options = lexOptions{
+		emitComment: true,
+		breakOK:     true,
+		continueOK:  true,
+	}
 	for {
 		item := l.nextItem()
 		items = append(items, item)
@@ -434,7 +438,9 @@ func TestLex(t *testing.T) {
 		items := collect(&test, "", "")
 		if !equal(items, test.items, false) {
 			t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
+			return // TODO
 		}
+		t.Log(test.name, "OK")
 	}
 }
 
@@ -472,6 +478,39 @@ func TestDelims(t *testing.T) {
 	}
 }
 
+func TestDelimsAlphaNumeric(t *testing.T) {
+	test := lexTest{"right delimiter with alphanumeric start", "{{hub .host hub}}", []item{
+		mkItem(itemLeftDelim, "{{hub"),
+		mkItem(itemSpace, " "),
+		mkItem(itemField, ".host"),
+		mkItem(itemSpace, " "),
+		mkItem(itemRightDelim, "hub}}"),
+		tEOF,
+	}}
+	items := collect(&test, "{{hub", "hub}}")
+
+	if !equal(items, test.items, false) {
+		t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
+	}
+}
+
+func TestDelimsAndMarkers(t *testing.T) {
+	test := lexTest{"delims that look like markers", "{{- .x -}} {{- - .x - -}}", []item{
+		mkItem(itemLeftDelim, "{{- "),
+		mkItem(itemField, ".x"),
+		mkItem(itemRightDelim, " -}}"),
+		mkItem(itemLeftDelim, "{{- "),
+		mkItem(itemField, ".x"),
+		mkItem(itemRightDelim, " -}}"),
+		tEOF,
+	}}
+	items := collect(&test, "{{- ", " -}}")
+
+	if !equal(items, test.items, false) {
+		t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
+	}
+}
+
 var lexPosTests = []lexTest{
 	{"empty", "", []item{{itemEOF, 0, "", 1}}},
 	{"punctuation", "{{,@%#}}", []item{
@@ -533,22 +572,6 @@ func TestPos(t *testing.T) {
 	}
 }
 
-// Test that an error shuts down the lexing goroutine.
-func TestShutdown(t *testing.T) {
-	// We need to duplicate template.Parse here to hold on to the lexer.
-	const text = "erroneous{{define}}{{else}}1234"
-	lexer := lex("foo", text, "{{", "}}", false)
-	_, err := New("root").parseLexer(lexer)
-	if err == nil {
-		t.Fatalf("expected error")
-	}
-	// The error should have drained the input. Therefore, the lexer should be shut down.
-	token, ok := <-lexer.items
-	if ok {
-		t.Fatalf("input was not drained; got %v", token)
-	}
-}
-
 // parseLexer is a local version of parse that lets us pass in the lexer instead of building it.
 // We expect an error, so the tree set and funcs list are explicitly nil.
 func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
index 52bd6aca276..6ab48b3f740 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
@@ -492,7 +492,7 @@ var errorTests = []parseTest{
 		hasError, `unclosed left paren`},
 	{"rparen",
 		"{{.X 1 2 3 ) }}",
-		hasError, `unexpected ")" in command`},
+		hasError, "unexpected right paren"},
 	{"rparen2",
 		"{{(.X 1 2 3",
 		hasError, `unclosed action`},
@@ -600,7 +600,8 @@ func TestBlock(t *testing.T) {
 }
 
 func TestLineNum(t *testing.T) {
-	const count = 100
+	// const count = 100
+	const count = 3
 	text := strings.Repeat("{{printf 1234}}\n", count)
 	tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
 	if err != nil {
@@ -614,11 +615,11 @@ func TestLineNum(t *testing.T) {
 		// Action first.
 		action := nodes[i].(*ActionNode)
 		if action.Line != line {
-			t.Fatalf("line %d: action is line %d", line, action.Line)
+			t.Errorf("line %d: action is line %d", line, action.Line)
 		}
 		pipe := action.Pipe
 		if pipe.Line != line {
-			t.Fatalf("line %d: pipe is line %d", line, pipe.Line)
+			t.Errorf("line %d: pipe is line %d", line, pipe.Line)
 		}
 	}
 }