URI: 
       Fix Go template script escaping - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit 6c3c6686f5d3c7155e2d455b07ac8ab70f42cb88
   DIR parent c34bf48560c91c8a2fa106867af7b08a569609b5
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Sat, 23 May 2020 15:32:27 +0200
       
       Fix Go template script escaping
       
       Fixes #6695
       
       Diffstat:
         M hugolib/template_test.go            |      18 ++++++++++++++++++
         M scripts/fork_go_templates/main.go   |      15 ++++++++++++++-
         A tpl/internal/go_templates/cfg/cfg.… |      64 +++++++++++++++++++++++++++++++
         M tpl/internal/go_templates/fmtsort/… |      14 +++++++++-----
         M tpl/internal/go_templates/fmtsort/… |       2 +-
         M tpl/internal/go_templates/htmltemp… |      70 ++++++++++++++++----------------
         M tpl/internal/go_templates/htmltemp… |      45 +++++++++++++++++++++++++++++++
         M tpl/internal/go_templates/htmltemp… |       6 +++---
         M tpl/internal/go_templates/htmltemp… |       6 +++---
         M tpl/internal/go_templates/htmltemp… |      72 ++++++++++++++++++-------------
         M tpl/internal/go_templates/htmltemp… |      68 ++++++++++++++++----------------
         M tpl/internal/go_templates/htmltemp… |      39 +++++++++++++++++++++++++++++++
         A tpl/internal/go_templates/testenv/… |     272 +++++++++++++++++++++++++++++++
         A tpl/internal/go_templates/testenv/… |      11 +++++++++++
         A tpl/internal/go_templates/testenv/… |      20 ++++++++++++++++++++
         A tpl/internal/go_templates/testenv/… |      48 +++++++++++++++++++++++++++++++
         M tpl/internal/go_templates/texttemp… |      18 ++++++++----------
         M tpl/internal/go_templates/texttemp… |      27 +++++++++++++++++----------
         M tpl/internal/go_templates/texttemp… |      93 ++++++++++++++++++++++++++++---
         M tpl/internal/go_templates/texttemp… |     219 +++++++++++++++++--------------
         M tpl/internal/go_templates/texttemp… |       2 +-
         M tpl/internal/go_templates/texttemp… |       2 +-
         M tpl/internal/go_templates/texttemp… |       1 -
         M tpl/internal/go_templates/texttemp… |     170 ++++++++++++++++++++++++-------
         M tpl/internal/go_templates/texttemp… |       9 ++-------
         M tpl/internal/go_templates/texttemp… |      52 ++++++++++++++++++++++++++++++-
         M tpl/internal/go_templates/texttemp… |      23 ++++++++++++-----------
       
       27 files changed, 1092 insertions(+), 294 deletions(-)
       ---
   DIR diff --git a/hugolib/template_test.go b/hugolib/template_test.go
       @@ -566,6 +566,24 @@ title: P1
        
        }
        
       +func TestTemplateGoIssues(t *testing.T) {
       +        b := newTestSitesBuilder(t)
       +
       +        b.WithTemplatesAdded(
       +                "index.html", `
       +{{ $title := "a & b" }}
       +<script type="application/ld+json">{"@type":"WebPage","headline":"{{$title}}"}</script>
       +`,
       +        )
       +
       +        b.Build(BuildCfg{})
       +
       +        b.AssertFileContent("public/index.html", `
       +<script type="application/ld+json">{"@type":"WebPage","headline":"a \u0026 b"}</script>
       +
       +`)
       +}
       +
        func collectIdentities(set map[identity.Identity]bool, provider identity.Provider) {
                if ids, ok := provider.(identity.IdentitiesProvider); ok {
                        for _, id := range ids.GetIdentities() {
   DIR diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
       @@ -17,7 +17,7 @@ import (
        
        func main() {
                // TODO(bep) git checkout tag
       -        // The current is built with Go version 9341fe073e6f7742c9d61982084874560dac2014 / go1.13.5
       +        // The current is built with Go version b68fa57c599720d33a2d735782969ce95eabf794 / go1.15dev
                fmt.Println("Forking ...")
                defer fmt.Println("Done ...")
        
       @@ -55,6 +55,8 @@ var (
                textTemplateReplacers = strings.NewReplacer(
                        `"text/template/`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/`,
                        `"internal/fmtsort"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`,
       +                `"internal/testenv"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`,
       +                "TestLinkerGC", "_TestLinkerGC",
                        // Rename types and function that we want to overload.
                        "type state struct", "type stateOld struct",
                        "func (s *state) evalFunction", "func (s *state) evalFunctionOld",
       @@ -63,6 +65,10 @@ var (
                        "func isTrue(val reflect.Value) (truth, ok bool) {", "func isTrueOld(val reflect.Value) (truth, ok bool) {",
                )
        
       +        testEnvReplacers = strings.NewReplacer(
       +                `"internal/cfg"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"`,
       +        )
       +
                htmlTemplateReplacers = strings.NewReplacer(
                        `. "html/template"`, `. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"`,
                        `"html/template"`, `template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"`,
       @@ -116,6 +122,13 @@ var goPackages = []goPackage{
                goPackage{srcPkg: "internal/fmtsort", dstPkg: "fmtsort", rewriter: func(name string) {
                        rewrite(name, `"internal/fmtsort" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`)
                }},
       +        goPackage{srcPkg: "internal/testenv", dstPkg: "testenv",
       +                replacer: func(name, content string) string { return testEnvReplacers.Replace(content) }, rewriter: func(name string) {
       +                        rewrite(name, `"internal/testenv" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`)
       +                }},
       +        goPackage{srcPkg: "internal/cfg", dstPkg: "cfg", rewriter: func(name string) {
       +                rewrite(name, `"internal/cfg" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"`)
       +        }},
        }
        
        var fs = afero.NewOsFs()
   DIR diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go
       @@ -0,0 +1,64 @@
       +// Copyright 2019 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 cfg holds configuration shared by the Go command and internal/testenv.
       +// Definitions that don't need to be exposed outside of cmd/go should be in
       +// cmd/go/internal/cfg instead of this package.
       +package cfg
       +
       +// KnownEnv is a list of environment variables that affect the operation
       +// of the Go command.
       +const KnownEnv = `
       +        AR
       +        CC
       +        CGO_CFLAGS
       +        CGO_CFLAGS_ALLOW
       +        CGO_CFLAGS_DISALLOW
       +        CGO_CPPFLAGS
       +        CGO_CPPFLAGS_ALLOW
       +        CGO_CPPFLAGS_DISALLOW
       +        CGO_CXXFLAGS
       +        CGO_CXXFLAGS_ALLOW
       +        CGO_CXXFLAGS_DISALLOW
       +        CGO_ENABLED
       +        CGO_FFLAGS
       +        CGO_FFLAGS_ALLOW
       +        CGO_FFLAGS_DISALLOW
       +        CGO_LDFLAGS
       +        CGO_LDFLAGS_ALLOW
       +        CGO_LDFLAGS_DISALLOW
       +        CXX
       +        FC
       +        GCCGO
       +        GO111MODULE
       +        GO386
       +        GOARCH
       +        GOARM
       +        GOBIN
       +        GOCACHE
       +        GOENV
       +        GOEXE
       +        GOFLAGS
       +        GOGCCFLAGS
       +        GOHOSTARCH
       +        GOHOSTOS
       +        GOINSECURE
       +        GOMIPS
       +        GOMIPS64
       +        GOMODCACHE
       +        GONOPROXY
       +        GONOSUMDB
       +        GOOS
       +        GOPATH
       +        GOPPC64
       +        GOPRIVATE
       +        GOPROXY
       +        GOROOT
       +        GOSUMDB
       +        GOTMPDIR
       +        GOTOOLDIR
       +        GOWASM
       +        GO_EXTLINK_ENABLED
       +        PKG_CONFIG
       +`
   DIR diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go
       @@ -53,12 +53,16 @@ func Sort(mapValue reflect.Value) *SortedMap {
                if mapValue.Type().Kind() != reflect.Map {
                        return nil
                }
       -        key := make([]reflect.Value, mapValue.Len())
       -        value := make([]reflect.Value, len(key))
       +        // Note: this code is arranged to not panic even in the presence
       +        // of a concurrent map update. The runtime is responsible for
       +        // yelling loudly if that happens. See issue 33275.
       +        n := mapValue.Len()
       +        key := make([]reflect.Value, 0, n)
       +        value := make([]reflect.Value, 0, n)
                iter := mapValue.MapRange()
       -        for i := 0; iter.Next(); i++ {
       -                key[i] = iter.Key()
       -                value[i] = iter.Value()
       +        for iter.Next() {
       +                key = append(key, iter.Key())
       +                value = append(value, iter.Value())
                }
                sorted := &SortedMap{
                        Key:   key,
   DIR diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go
       @@ -119,7 +119,7 @@ var sortTests = []sortTest{
                        "PTR0:0 PTR1:1 PTR2:2",
                },
                {
       -                map[toy]string{toy{7, 2}: "72", toy{7, 1}: "71", toy{3, 4}: "34"},
       +                map[toy]string{{7, 2}: "72", {7, 1}: "71", {3, 4}: "34"},
                        "{3 4}:34 {7 1}:71 {7 2}:72",
                },
                {
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/content_test.go b/tpl/internal/go_templates/htmltemplate/content_test.go
       @@ -21,7 +21,7 @@ func TestTypedContent(t *testing.T) {
                        htmltemplate.HTML(`Hello, <b>World</b> &amp;tc!`),
                        htmltemplate.HTMLAttr(` dir="ltr"`),
                        htmltemplate.JS(`c && alert("Hello, World!");`),
       -                htmltemplate.JSStr(`Hello, World & O'Reilly\x21`),
       +                htmltemplate.JSStr(`Hello, World & O'Reilly\u0021`),
                        htmltemplate.URL(`greeting=H%69,&addressee=(World)`),
                        htmltemplate.Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
                        htmltemplate.URL(`,foo/,`),
       @@ -73,7 +73,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello, <b>World</b> &amp;tc!`,
                                        ` dir=&#34;ltr&#34;`,
                                        `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
       -                                `Hello, World &amp; O&#39;Reilly\x21`,
       +                                `Hello, World &amp; O&#39;Reilly\u0021`,
                                        `greeting=H%69,&amp;addressee=(World)`,
                                        `greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
                                        `,foo/,`,
       @@ -103,7 +103,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello,&#32;World&#32;&amp;tc!`,
                                        `&#32;dir&#61;&#34;ltr&#34;`,
                                        `c&#32;&amp;&amp;&#32;alert(&#34;Hello,&#32;World!&#34;);`,
       -                                `Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\x21`,
       +                                `Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\u0021`,
                                        `greeting&#61;H%69,&amp;addressee&#61;(World)`,
                                        `greeting&#61;H%69,&amp;addressee&#61;(World)&#32;2x,&#32;https://golang.org/favicon.ico&#32;500.5w`,
                                        `,foo/,`,
       @@ -118,7 +118,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello, World &amp;tc!`,
                                        ` dir=&#34;ltr&#34;`,
                                        `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
       -                                `Hello, World &amp; O&#39;Reilly\x21`,
       +                                `Hello, World &amp; O&#39;Reilly\u0021`,
                                        `greeting=H%69,&amp;addressee=(World)`,
                                        `greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
                                        `,foo/,`,
       @@ -133,7 +133,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello, &lt;b&gt;World&lt;/b&gt; &amp;tc!`,
                                        ` dir=&#34;ltr&#34;`,
                                        `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
       -                                `Hello, World &amp; O&#39;Reilly\x21`,
       +                                `Hello, World &amp; O&#39;Reilly\u0021`,
                                        `greeting=H%69,&amp;addressee=(World)`,
                                        `greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
                                        `,foo/,`,
       @@ -149,7 +149,7 @@ func TestTypedContent(t *testing.T) {
                                        // Not escaped.
                                        `c && alert("Hello, World!");`,
                                        // Escape sequence not over-escaped.
       -                                `"Hello, World & O'Reilly\x21"`,
       +                                `"Hello, World & O'Reilly\u0021"`,
                                        `"greeting=H%69,\u0026addressee=(World)"`,
                                        `"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
                                        `",foo/,"`,
       @@ -165,7 +165,7 @@ func TestTypedContent(t *testing.T) {
                                        // Not JS escaped but HTML escaped.
                                        `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
                                        // Escape sequence not over-escaped.
       -                                `&#34;Hello, World &amp; O&#39;Reilly\x21&#34;`,
       +                                `&#34;Hello, World &amp; O&#39;Reilly\u0021&#34;`,
                                        `&#34;greeting=H%69,\u0026addressee=(World)&#34;`,
                                        `&#34;greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w&#34;`,
                                        `&#34;,foo/,&#34;`,
       @@ -174,30 +174,30 @@ func TestTypedContent(t *testing.T) {
                        {
                                `<script>alert("{{.}}")</script>`,
                                []string{
       -                                `\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
       -                                `a[href =~ \x22\/\/example.com\x22]#foo`,
       -                                `Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
       -                                ` dir=\x22ltr\x22`,
       -                                `c \x26\x26 alert(\x22Hello, World!\x22);`,
       +                                `\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
       +                                `a[href =~ \u0022\/\/example.com\u0022]#foo`,
       +                                `Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
       +                                ` dir=\u0022ltr\u0022`,
       +                                `c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
                                        // Escape sequence not over-escaped.
       -                                `Hello, World \x26 O\x27Reilly\x21`,
       -                                `greeting=H%69,\x26addressee=(World)`,
       -                                `greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
       +                                `Hello, World \u0026 O\u0027Reilly\u0021`,
       +                                `greeting=H%69,\u0026addressee=(World)`,
       +                                `greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
                                        `,foo\/,`,
                                },
                        },
                        {
                                `<script type="text/javascript">alert("{{.}}")</script>`,
                                []string{
       -                                `\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
       -                                `a[href =~ \x22\/\/example.com\x22]#foo`,
       -                                `Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
       -                                ` dir=\x22ltr\x22`,
       -                                `c \x26\x26 alert(\x22Hello, World!\x22);`,
       +                                `\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
       +                                `a[href =~ \u0022\/\/example.com\u0022]#foo`,
       +                                `Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
       +                                ` dir=\u0022ltr\u0022`,
       +                                `c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
                                        // Escape sequence not over-escaped.
       -                                `Hello, World \x26 O\x27Reilly\x21`,
       -                                `greeting=H%69,\x26addressee=(World)`,
       -                                `greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
       +                                `Hello, World \u0026 O\u0027Reilly\u0021`,
       +                                `greeting=H%69,\u0026addressee=(World)`,
       +                                `greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
                                        `,foo\/,`,
                                },
                        },
       @@ -211,7 +211,7 @@ func TestTypedContent(t *testing.T) {
                                        // Not escaped.
                                        `c && alert("Hello, World!");`,
                                        // Escape sequence not over-escaped.
       -                                `"Hello, World & O'Reilly\x21"`,
       +                                `"Hello, World & O'Reilly\u0021"`,
                                        `"greeting=H%69,\u0026addressee=(World)"`,
                                        `"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
                                        `",foo/,"`,
       @@ -227,7 +227,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello, <b>World</b> &amp;tc!`,
                                        ` dir=&#34;ltr&#34;`,
                                        `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
       -                                `Hello, World &amp; O&#39;Reilly\x21`,
       +                                `Hello, World &amp; O&#39;Reilly\u0021`,
                                        `greeting=H%69,&amp;addressee=(World)`,
                                        `greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
                                        `,foo/,`,
       @@ -236,15 +236,15 @@ func TestTypedContent(t *testing.T) {
                        {
                                `<button onclick='alert("{{.}}")'>`,
                                []string{
       -                                `\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
       -                                `a[href =~ \x22\/\/example.com\x22]#foo`,
       -                                `Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
       -                                ` dir=\x22ltr\x22`,
       -                                `c \x26\x26 alert(\x22Hello, World!\x22);`,
       +                                `\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
       +                                `a[href =~ \u0022\/\/example.com\u0022]#foo`,
       +                                `Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
       +                                ` dir=\u0022ltr\u0022`,
       +                                `c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
                                        // Escape sequence not over-escaped.
       -                                `Hello, World \x26 O\x27Reilly\x21`,
       -                                `greeting=H%69,\x26addressee=(World)`,
       -                                `greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
       +                                `Hello, World \u0026 O\u0027Reilly\u0021`,
       +                                `greeting=H%69,\u0026addressee=(World)`,
       +                                `greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
                                        `,foo\/,`,
                                },
                        },
       @@ -256,7 +256,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
                                        `%20dir%3d%22ltr%22`,
                                        `c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
       -                                `Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
       +                                `Hello%2c%20World%20%26%20O%27Reilly%5cu0021`,
                                        // Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done.
                                        `greeting=H%69,&amp;addressee=%28World%29`,
                                        `greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,
       @@ -271,7 +271,7 @@ func TestTypedContent(t *testing.T) {
                                        `Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
                                        `%20dir%3d%22ltr%22`,
                                        `c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
       -                                `Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
       +                                `Hello%2c%20World%20%26%20O%27Reilly%5cu0021`,
                                        // Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done.
                                        `greeting=H%69,&addressee=%28World%29`,
                                        `greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go
       @@ -73,6 +73,51 @@ 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
       +
       +Attributes with a namespace are treated as if they had no namespace.
       +Given the excerpt
       +
       +  <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>
       +
       +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>
       +
       +At parse time this becomes
       +
       +  <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>
       +
       +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>
       +
       +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>
       +
       +At parse time they become:
       +
       +  <a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
       +  <a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
       +  <a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
       +
        Errors
        
        See the documentation of ErrorCode for details.
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go
       @@ -242,7 +242,7 @@ func TestEscape(t *testing.T) {
                        {
                                "jsStr",
                                "<button onclick='alert(&quot;{{.H}}&quot;)'>",
       -                        `<button onclick='alert(&quot;\x3cHello\x3e&quot;)'>`,
       +                        `<button onclick='alert(&quot;\u003cHello\u003e&quot;)'>`,
                        },
                        {
                                "badMarshaler",
       @@ -263,7 +263,7 @@ func TestEscape(t *testing.T) {
                        {
                                "jsRe",
                                `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
       -                        `<button onclick='alert(/foo\x2bbar/.test(""))'>`,
       +                        `<button onclick='alert(/foo\u002bbar/.test(""))'>`,
                        },
                        {
                                "jsReBlank",
       @@ -829,7 +829,7 @@ func TestEscapeSet(t *testing.T) {
                                        "main":   `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
                                        "helper": `{{11}} of {{"<100>"}}`,
                                },
       -                        `<button onclick="title='11 of \x3c100\x3e'; ...">11 of &lt;100&gt;</button>`,
       +                        `<button onclick="title='11 of \u003c100\u003e'; ...">11 of &lt;100&gt;</button>`,
                        },
                        // A non-recursive template that ends in a different context.
                        // helper starts in jsCtxRegexp and ends in jsCtxDivOp.
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/example_test.go b/tpl/internal/go_templates/htmltemplate/example_test.go
       @@ -119,9 +119,9 @@ func Example_escape() {
                // &#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
                // &#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
                // &#34;Fran &amp; Freddie&#39;s Diner&#34;32&lt;tasty@example.com&gt;
       -        // \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
       -        // \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
       -        // \"Fran & Freddie\'s Diner\"32\x3Ctasty@example.com\x3E
       +        // \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
       +        // \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
       +        // \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
                // %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
        
        }
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go
       @@ -164,7 +164,6 @@ func jsValEscaper(args ...interface{}) string {
                }
                // TODO: detect cycles before calling Marshal which loops infinitely on
                // cyclic data. This may be an unacceptable DoS risk.
       -
                b, err := json.Marshal(a)
                if err != nil {
                        // Put a space before comment so that if it is flush against
       @@ -179,8 +178,8 @@ func jsValEscaper(args ...interface{}) string {
                // TODO: maybe post-process output to prevent it from containing
                // "<!--", "-->", "<![CDATA[", "]]>", or "</script"
                // in case custom marshalers produce output containing those.
       -
       -        // TODO: Maybe abbreviate \u00ab to \xab to produce more compact output.
       +        // Note: Do not use \x escaping to save bytes because it is not JSON compatible and this escaper
       +        // supports ld+json content-type.
                if len(b) == 0 {
                        // In, `x=y/{{.}}*z` a json.Marshaler that produces "" should
                        // not cause the output `x=y/*z`.
       @@ -261,6 +260,8 @@ func replace(s string, replacementTable []string) string {
                        r, w = utf8.DecodeRuneInString(s[i:])
                        var repl string
                        switch {
       +                case int(r) < len(lowUnicodeReplacementTable):
       +                        repl = lowUnicodeReplacementTable[r]
                        case int(r) < len(replacementTable) && replacementTable[r] != "":
                                repl = replacementTable[r]
                        case r == '\u2028':
       @@ -284,67 +285,80 @@ func replace(s string, replacementTable []string) string {
                return b.String()
        }
        
       +var lowUnicodeReplacementTable = []string{
       +        0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
       +        '\a': `\u0007`,
       +        '\b': `\u0008`,
       +        '\t': `\t`,
       +        '\n': `\n`,
       +        '\v': `\u000b`, // "\v" == "v" on IE 6.
       +        '\f': `\f`,
       +        '\r': `\r`,
       +        0xe:  `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`,
       +        0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`,
       +        0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`,
       +}
       +
        var jsStrReplacementTable = []string{
       -        0:    `\0`,
       +        0:    `\u0000`,
                '\t': `\t`,
                '\n': `\n`,
       -        '\v': `\x0b`, // "\v" == "v" on IE 6.
       +        '\v': `\u000b`, // "\v" == "v" on IE 6.
                '\f': `\f`,
                '\r': `\r`,
                // Encode HTML specials as hex so the output can be embedded
                // in HTML attributes without further encoding.
       -        '"':  `\x22`,
       -        '&':  `\x26`,
       -        '\'': `\x27`,
       -        '+':  `\x2b`,
       +        '"':  `\u0022`,
       +        '&':  `\u0026`,
       +        '\'': `\u0027`,
       +        '+':  `\u002b`,
                '/':  `\/`,
       -        '<':  `\x3c`,
       -        '>':  `\x3e`,
       +        '<':  `\u003c`,
       +        '>':  `\u003e`,
                '\\': `\\`,
        }
        
        // jsStrNormReplacementTable is like jsStrReplacementTable but does not
        // overencode existing escapes since this table has no entry for `\`.
        var jsStrNormReplacementTable = []string{
       -        0:    `\0`,
       +        0:    `\u0000`,
                '\t': `\t`,
                '\n': `\n`,
       -        '\v': `\x0b`, // "\v" == "v" on IE 6.
       +        '\v': `\u000b`, // "\v" == "v" on IE 6.
                '\f': `\f`,
                '\r': `\r`,
                // Encode HTML specials as hex so the output can be embedded
                // in HTML attributes without further encoding.
       -        '"':  `\x22`,
       -        '&':  `\x26`,
       -        '\'': `\x27`,
       -        '+':  `\x2b`,
       +        '"':  `\u0022`,
       +        '&':  `\u0026`,
       +        '\'': `\u0027`,
       +        '+':  `\u002b`,
                '/':  `\/`,
       -        '<':  `\x3c`,
       -        '>':  `\x3e`,
       +        '<':  `\u003c`,
       +        '>':  `\u003e`,
        }
       -
        var jsRegexpReplacementTable = []string{
       -        0:    `\0`,
       +        0:    `\u0000`,
                '\t': `\t`,
                '\n': `\n`,
       -        '\v': `\x0b`, // "\v" == "v" on IE 6.
       +        '\v': `\u000b`, // "\v" == "v" on IE 6.
                '\f': `\f`,
                '\r': `\r`,
                // Encode HTML specials as hex so the output can be embedded
                // in HTML attributes without further encoding.
       -        '"':  `\x22`,
       +        '"':  `\u0022`,
                '$':  `\$`,
       -        '&':  `\x26`,
       -        '\'': `\x27`,
       +        '&':  `\u0026`,
       +        '\'': `\u0027`,
                '(':  `\(`,
                ')':  `\)`,
                '*':  `\*`,
       -        '+':  `\x2b`,
       +        '+':  `\u002b`,
                '-':  `\-`,
                '.':  `\.`,
                '/':  `\/`,
       -        '<':  `\x3c`,
       -        '>':  `\x3e`,
       +        '<':  `\u003c`,
       +        '>':  `\u003e`,
                '?':  `\?`,
                '[':  `\[`,
                '\\': `\\`,
       @@ -384,11 +398,11 @@ func isJSType(mimeType string) bool {
                //   https://tools.ietf.org/html/rfc7231#section-3.1.1
                //   https://tools.ietf.org/html/rfc4329#section-3
                //   https://www.ietf.org/rfc/rfc4627.txt
       -        mimeType = strings.ToLower(mimeType)
                // discard parameters
                if i := strings.Index(mimeType, ";"); i >= 0 {
                        mimeType = mimeType[:i]
                }
       +        mimeType = strings.ToLower(mimeType)
                mimeType = strings.TrimSpace(mimeType)
                switch mimeType {
                case
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go
       @@ -139,7 +139,7 @@ func TestJSValEscaper(t *testing.T) {
                        {"foo", `"foo"`},
                        // Newlines.
                        {"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
       -                // "\v" == "v" on IE 6 so use "\x0b" instead.
       +                // "\v" == "v" on IE 6 so use "\u000b" instead.
                        {"\t\x0b", `"\t\u000b"`},
                        {struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
                        {[]interface{}{}, "[]"},
       @@ -175,7 +175,7 @@ func TestJSStrEscaper(t *testing.T) {
                }{
                        {"", ``},
                        {"foo", `foo`},
       -                {"\u0000", `\0`},
       +                {"\u0000", `\u0000`},
                        {"\t", `\t`},
                        {"\n", `\n`},
                        {"\r", `\r`},
       @@ -185,14 +185,14 @@ func TestJSStrEscaper(t *testing.T) {
                        {"\\n", `\\n`},
                        {"foo\r\nbar", `foo\r\nbar`},
                        // Preserve attribute boundaries.
       -                {`"`, `\x22`},
       -                {`'`, `\x27`},
       +                {`"`, `\u0022`},
       +                {`'`, `\u0027`},
                        // Allow embedding in HTML without further escaping.
       -                {`&amp;`, `\x26amp;`},
       +                {`&amp;`, `\u0026amp;`},
                        // Prevent breaking out of text node and element boundaries.
       -                {"</script>", `\x3c\/script\x3e`},
       -                {"<![CDATA[", `\x3c![CDATA[`},
       -                {"]]>", `]]\x3e`},
       +                {"</script>", `\u003c\/script\u003e`},
       +                {"<![CDATA[", `\u003c![CDATA[`},
       +                {"]]>", `]]\u003e`},
                        // https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
                        //   "The text in style, script, title, and textarea elements
                        //   must not have an escaping text span start that is not
       @@ -203,11 +203,11 @@ func TestJSStrEscaper(t *testing.T) {
                        // allow regular text content to be interpreted as script
                        // allowing script execution via a combination of a JS string
                        // injection followed by an HTML text injection.
       -                {"<!--", `\x3c!--`},
       -                {"-->", `--\x3e`},
       +                {"<!--", `\u003c!--`},
       +                {"-->", `--\u003e`},
                        // From https://code.google.com/p/doctype/wiki/ArticleUtf7
                        {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
       -                        `\x2bADw-script\x2bAD4-alert(1)\x2bADw-\/script\x2bAD4-`,
       +                        `\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
                        },
                        // Invalid UTF-8 sequence
                        {"foo\xA0bar", "foo\xA0bar"},
       @@ -230,7 +230,7 @@ func TestJSRegexpEscaper(t *testing.T) {
                }{
                        {"", `(?:)`},
                        {"foo", `foo`},
       -                {"\u0000", `\0`},
       +                {"\u0000", `\u0000`},
                        {"\t", `\t`},
                        {"\n", `\n`},
                        {"\r", `\r`},
       @@ -240,19 +240,19 @@ func TestJSRegexpEscaper(t *testing.T) {
                        {"\\n", `\\n`},
                        {"foo\r\nbar", `foo\r\nbar`},
                        // Preserve attribute boundaries.
       -                {`"`, `\x22`},
       -                {`'`, `\x27`},
       +                {`"`, `\u0022`},
       +                {`'`, `\u0027`},
                        // Allow embedding in HTML without further escaping.
       -                {`&amp;`, `\x26amp;`},
       +                {`&amp;`, `\u0026amp;`},
                        // Prevent breaking out of text node and element boundaries.
       -                {"</script>", `\x3c\/script\x3e`},
       -                {"<![CDATA[", `\x3c!\[CDATA\[`},
       -                {"]]>", `\]\]\x3e`},
       +                {"</script>", `\u003c\/script\u003e`},
       +                {"<![CDATA[", `\u003c!\[CDATA\[`},
       +                {"]]>", `\]\]\u003e`},
                        // Escaping text spans.
       -                {"<!--", `\x3c!\-\-`},
       -                {"-->", `\-\-\x3e`},
       +                {"<!--", `\u003c!\-\-`},
       +                {"-->", `\-\-\u003e`},
                        {"*", `\*`},
       -                {"+", `\x2b`},
       +                {"+", `\u002b`},
                        {"?", `\?`},
                        {"[](){}", `\[\]\(\)\{\}`},
                        {"$foo|x.y", `\$foo\|x\.y`},
       @@ -286,27 +286,27 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
                        {
                                "jsStrEscaper",
                                jsStrEscaper,
       -                        "\\0\x01\x02\x03\x04\x05\x06\x07" +
       -                                "\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
       -                                "\x10\x11\x12\x13\x14\x15\x16\x17" +
       -                                "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
       -                                ` !\x22#$%\x26\x27()*\x2b,-.\/` +
       -                                `0123456789:;\x3c=\x3e?` +
       +                        `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
       +                                `\u0008\t\n\u000b\f\r\u000e\u000f` +
       +                                `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
       +                                `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
       +                                ` !\u0022#$%\u0026\u0027()*\u002b,-.\/` +
       +                                `0123456789:;\u003c=\u003e?` +
                                        `@ABCDEFGHIJKLMNO` +
                                        `PQRSTUVWXYZ[\\]^_` +
                                        "`abcdefghijklmno" +
       -                                "pqrstuvwxyz{|}~\x7f" +
       +                                "pqrstuvwxyz{|}~\u007f" +
                                        "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
                        },
                        {
                                "jsRegexpEscaper",
                                jsRegexpEscaper,
       -                        "\\0\x01\x02\x03\x04\x05\x06\x07" +
       -                                "\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
       -                                "\x10\x11\x12\x13\x14\x15\x16\x17" +
       -                                "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
       -                                ` !\x22#\$%\x26\x27\(\)\*\x2b,\-\.\/` +
       -                                `0123456789:;\x3c=\x3e\?` +
       +                        `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
       +                                `\u0008\t\n\u000b\f\r\u000e\u000f` +
       +                                `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
       +                                `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
       +                                ` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` +
       +                                `0123456789:;\u003c=\u003e\?` +
                                        `@ABCDEFGHIJKLMNO` +
                                        `PQRSTUVWXYZ\[\\\]\^_` +
                                        "`abcdefghijklmno" +
   DIR diff --git a/tpl/internal/go_templates/htmltemplate/template_test.go b/tpl/internal/go_templates/htmltemplate/template_test.go
       @@ -8,6 +8,7 @@ package template_test
        
        import (
                "bytes"
       +        "encoding/json"
                "strings"
                "testing"
        
       @@ -124,6 +125,44 @@ func TestNumbers(t *testing.T) {
                c.mustExecute(c.root, nil, "12.34 7.5")
        }
        
       +func TestStringsInScriptsWithJsonContentTypeAreCorrectlyEscaped(t *testing.T) {
       +        // See #33671 and #37634 for more context on this.
       +        tests := []struct{ name, in string }{
       +                {"empty", ""},
       +                {"invalid", string(rune(-1))},
       +                {"null", "\u0000"},
       +                {"unit separator", "\u001F"},
       +                {"tab", "\t"},
       +                {"gt and lt", "<>"},
       +                {"quotes", `'"`},
       +                {"ASCII letters", "ASCII letters"},
       +                {"Unicode", "ʕ⊙ϖ⊙ʔ"},
       +                {"Pizza", "🍕"},
       +        }
       +        const (
       +                prefix = `<script type="application/ld+json">`
       +                suffix = `</script>`
       +                templ  = prefix + `"{{.}}"` + suffix
       +        )
       +        tpl := Must(New("JS string is JSON string").Parse(templ))
       +        for _, tt := range tests {
       +                t.Run(tt.name, func(t *testing.T) {
       +                        var buf bytes.Buffer
       +                        if err := tpl.Execute(&buf, tt.in); err != nil {
       +                                t.Fatalf("Cannot render template: %v", err)
       +                        }
       +                        trimmed := bytes.TrimSuffix(bytes.TrimPrefix(buf.Bytes(), []byte(prefix)), []byte(suffix))
       +                        var got string
       +                        if err := json.Unmarshal(trimmed, &got); err != nil {
       +                                t.Fatalf("Cannot parse JS string %q as JSON: %v", trimmed[1:len(trimmed)-1], err)
       +                        }
       +                        if got != tt.in {
       +                                t.Errorf("Serialization changed the string value: got %q want %q", got, tt.in)
       +                        }
       +                })
       +        }
       +}
       +
        type testCase struct {
                t    *testing.T
                root *Template
   DIR diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
       @@ -0,0 +1,272 @@
       +// 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 provides information about what functionality
       +// is available in different testing environments run by the Go team.
       +//
       +// It is an internal package because these details are specific
       +// to the Go team's test setup (on build.golang.org) and not
       +// fundamental to tests in general.
       +package testenv
       +
       +import (
       +        "errors"
       +        "flag"
       +        "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
       +        "os"
       +        "os/exec"
       +        "path/filepath"
       +        "runtime"
       +        "strconv"
       +        "strings"
       +        "sync"
       +        "testing"
       +)
       +
       +// Builder reports the name of the builder running this test
       +// (for example, "linux-amd64" or "windows-386-gce").
       +// If the test is not running on the build infrastructure,
       +// Builder returns the empty string.
       +func Builder() string {
       +        return os.Getenv("GO_BUILDER_NAME")
       +}
       +
       +// 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") != "" {
       +                // It's too much work to require every caller of the go command
       +                // to pass along "-gcflags="+os.Getenv("GO_GCFLAGS").
       +                // For now, if $GO_GCFLAGS is set, report that we simply can't
       +                // run go build.
       +                return false
       +        }
       +        switch runtime.GOOS {
       +        case "android", "js":
       +                return false
       +        case "darwin":
       +                if runtime.GOARCH == "arm64" {
       +                        return false
       +                }
       +        }
       +        return true
       +}
       +
       +// 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) {
       +        if os.Getenv("GO_GCFLAGS") != "" {
       +                t.Skipf("skipping test: 'go build' not compatible with setting $GO_GCFLAGS")
       +        }
       +        if !HasGoBuild() {
       +                t.Skipf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
       +        }
       +}
       +
       +// 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.''
       +// If not, MustHaveGoRun calls t.Skip with an explanation.
       +func MustHaveGoRun(t testing.TB) {
       +        if !HasGoRun() {
       +                t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
       +        }
       +}
       +
       +// GoToolPath reports the path to the Go tool.
       +// It is a convenience wrapper around GoTool.
       +// If the tool is unavailable GoToolPath calls t.Skip.
       +// If the tool should be available and isn't, GoToolPath calls t.Fatal.
       +func GoToolPath(t testing.TB) string {
       +        MustHaveGoBuild(t)
       +        path, err := GoTool()
       +        if err != nil {
       +                t.Fatal(err)
       +        }
       +        // Add all environment variables that affect the Go command to test metadata.
       +        // Cached test results will be invalidate when these variables change.
       +        // See golang.org/issue/32285.
       +        for _, envVar := range strings.Fields(cfg.KnownEnv) {
       +                os.Getenv(envVar)
       +        }
       +        return path
       +}
       +
       +// GoTool reports the path to the Go tool.
       +func GoTool() (string, error) {
       +        if !HasGoBuild() {
       +                return "", errors.New("platform cannot run go tool")
       +        }
       +        var exeSuffix string
       +        if runtime.GOOS == "windows" {
       +                exeSuffix = ".exe"
       +        }
       +        path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix)
       +        if _, err := os.Stat(path); err == nil {
       +                return path, nil
       +        }
       +        goBin, err := exec.LookPath("go" + exeSuffix)
       +        if err != nil {
       +                return "", errors.New("cannot find go tool: " + err.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":
       +                return false
       +        case "darwin":
       +                if runtime.GOARCH == "arm64" {
       +                        return false
       +                }
       +        }
       +        return true
       +}
       +
       +// HasSrc reports whether the entire source tree is available under GOROOT.
       +func HasSrc() bool {
       +        switch runtime.GOOS {
       +        case "darwin":
       +                if runtime.GOARCH == "arm64" {
       +                        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)
       +        }
       +}
       +
       +// HasExternalNetwork reports whether the current system can use
       +// external (non-localhost) networks.
       +func HasExternalNetwork() bool {
       +        return !testing.Short() && runtime.GOOS != "js"
       +}
       +
       +// MustHaveExternalNetwork checks that the current system can use
       +// external (non-localhost) networks.
       +// If not, MustHaveExternalNetwork calls t.Skip with an explanation.
       +func MustHaveExternalNetwork(t testing.TB) {
       +        if runtime.GOOS == "js" {
       +                t.Skipf("skipping test: no external network on %s", runtime.GOOS)
       +        }
       +        if testing.Short() {
       +                t.Skipf("skipping test: no external network in -short mode")
       +        }
       +}
       +
       +var haveCGO bool
       +
       +// HasCGO reports whether the current system can use cgo.
       +func HasCGO() bool {
       +        return haveCGO
       +}
       +
       +// MustHaveCGO calls t.Skip if cgo is not available.
       +func MustHaveCGO(t testing.TB) {
       +        if !haveCGO {
       +                t.Skipf("skipping test: no cgo")
       +        }
       +}
       +
       +// HasSymlink reports whether the current system can use os.Symlink.
       +func HasSymlink() bool {
       +        ok, _ := hasSymlink()
       +        return ok
       +}
       +
       +// MustHaveSymlink reports whether the current system can use os.Symlink.
       +// If not, MustHaveSymlink calls t.Skip with an explanation.
       +func MustHaveSymlink(t testing.TB) {
       +        ok, reason := hasSymlink()
       +        if !ok {
       +                t.Skipf("skipping test: cannot make symlinks on %s/%s%s", runtime.GOOS, runtime.GOARCH, reason)
       +        }
       +}
       +
       +// HasLink reports whether the current system can use os.Link.
       +func HasLink() bool {
       +        // From Android release M (Marshmallow), hard linking files is blocked
       +        // and an attempt to call link() on a file will return EACCES.
       +        // - https://code.google.com/p/android-developer-preview/issues/detail?id=3150
       +        return runtime.GOOS != "plan9" && runtime.GOOS != "android"
       +}
       +
       +// MustHaveLink reports whether the current system can use os.Link.
       +// If not, MustHaveLink calls t.Skip with an explanation.
       +func MustHaveLink(t testing.TB) {
       +        if !HasLink() {
       +                t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
       +        }
       +}
       +
       +var flaky = flag.Bool("flaky", false, "run known-flaky tests too")
       +
       +func SkipFlaky(t testing.TB, issue int) {
       +        t.Helper()
       +        if !*flaky {
       +                t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue)
       +        }
       +}
       +
       +func SkipFlakyNet(t testing.TB) {
       +        t.Helper()
       +        if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v {
       +                t.Skip("skipping test on builder known to have frequent network failures")
       +        }
       +}
       +
       +// 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
       +}
   DIR diff --git a/tpl/internal/go_templates/testenv/testenv_cgo.go b/tpl/internal/go_templates/testenv/testenv_cgo.go
       @@ -0,0 +1,11 @@
       +// Copyright 2017 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.
       +
       +// +build cgo
       +
       +package testenv
       +
       +func init() {
       +        haveCGO = true
       +}
   DIR diff --git a/tpl/internal/go_templates/testenv/testenv_notwin.go b/tpl/internal/go_templates/testenv/testenv_notwin.go
       @@ -0,0 +1,20 @@
       +// Copyright 2016 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.
       +
       +// +build !windows
       +
       +package testenv
       +
       +import (
       +        "runtime"
       +)
       +
       +func hasSymlink() (ok bool, reason string) {
       +        switch runtime.GOOS {
       +        case "android", "plan9":
       +                return false, ""
       +        }
       +
       +        return true, ""
       +}
   DIR diff --git a/tpl/internal/go_templates/testenv/testenv_windows.go b/tpl/internal/go_templates/testenv/testenv_windows.go
       @@ -0,0 +1,48 @@
       +// Copyright 2016 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 (
       +        "io/ioutil"
       +        "os"
       +        "path/filepath"
       +        "sync"
       +        "syscall"
       +)
       +
       +var symlinkOnce sync.Once
       +var winSymlinkErr error
       +
       +func initWinHasSymlink() {
       +        tmpdir, err := ioutil.TempDir("", "symtest")
       +        if err != nil {
       +                panic("failed to create temp directory: " + err.Error())
       +        }
       +        defer os.RemoveAll(tmpdir)
       +
       +        err = os.Symlink("target", filepath.Join(tmpdir, "symlink"))
       +        if err != nil {
       +                err = err.(*os.LinkError).Err
       +                switch err {
       +                case syscall.EWINDOWS, syscall.ERROR_PRIVILEGE_NOT_HELD:
       +                        winSymlinkErr = err
       +                }
       +        }
       +}
       +
       +func hasSymlink() (ok bool, reason string) {
       +        symlinkOnce.Do(initWinHasSymlink)
       +
       +        switch winSymlinkErr {
       +        case nil:
       +                return true, ""
       +        case syscall.EWINDOWS:
       +                return false, ": symlinks are not supported on your version of Windows"
       +        case syscall.ERROR_PRIVILEGE_NOT_HELD:
       +                return false, ": you don't have enough privileges to create symlinks"
       +        }
       +
       +        return false, ""
       +}
   DIR diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go
       @@ -102,8 +102,8 @@ data, defined in detail in the corresponding sections that follow.
                        If the value of the pipeline has length zero, nothing is output;
                        otherwise, dot is set to the successive elements of the array,
                        slice, or map and T1 is executed. If the value is a map and the
       -                keys are of basic type with a defined order ("comparable"), the
       -                elements will be visited in sorted key order.
       +                keys are of basic type with a defined order, the elements will be
       +                visited in sorted key order.
        
                {{range pipeline}} T1 {{else}} T0 {{end}}
                        The value of the pipeline must be an array, slice, map, or channel.
       @@ -385,14 +385,12 @@ returning in effect
        (Unlike with || in Go, however, eq is a function call and all the
        arguments will be evaluated.)
        
       -The comparison functions work on basic types only (or named basic
       -types, such as "type Celsius float32"). They implement the Go rules
       -for comparison of values, except that size and exact type are
       -ignored, so any integer value, signed or unsigned, may be compared
       -with any other integer value. (The arithmetic value is compared,
       -not the bit pattern, so all negative integers are less than all
       -unsigned integers.) However, as usual, one may not compare an int
       -with a float32 and so on.
       +The comparison functions work on any values whose type Go defines as
       +comparable. For basic types such as integers, the rules are relaxed:
       +size and exact type are ignored, so any integer value, signed or unsigned,
       +may be compared with any other integer value. (The arithmetic value is compared,
       +not the bit pattern, so all negative integers are less than all unsigned integers.)
       +However, as usual, one may not compare an int with a float32 and so on.
        
        Associated templates
        
   DIR diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
       @@ -5,7 +5,6 @@
        package template
        
        import (
       -        "bytes"
                "fmt"
                "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
                "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
       @@ -230,21 +229,19 @@ func (t *Template) DefinedTemplates() string {
                if t.common == nil {
                        return ""
                }
       -        var b bytes.Buffer
       +        var b strings.Builder
                for name, tmpl := range t.tmpl {
                        if tmpl.Tree == nil || tmpl.Root == nil {
                                continue
                        }
       -                if b.Len() > 0 {
       +                if b.Len() == 0 {
       +                        b.WriteString("; defined templates are: ")
       +                } else {
                                b.WriteString(", ")
                        }
                        fmt.Fprintf(&b, "%q", name)
                }
       -        var s string
       -        if b.Len() > 0 {
       -                s = "; defined templates are: " + b.String()
       -        }
       -        return s
       +        return b.String()
        }
        
        // Walk functions step through the major pieces of the template structure,
       @@ -464,7 +461,8 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
                        // Must be a function.
                        return s.evalFunction(dot, n, cmd, cmd.Args, final)
                case *parse.PipeNode:
       -                // Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
       +                // Parenthesized pipeline. The arguments are all inside the pipeline; final must be absent.
       +                s.notAFunction(cmd.Args, final)
                        return s.evalPipeline(dot, n)
                case *parse.VariableNode:
                        return s.evalVariableNode(dot, n, cmd.Args, final)
       @@ -499,20 +497,29 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
                switch {
                case constant.IsComplex:
                        return reflect.ValueOf(constant.Complex128) // incontrovertible.
       -        case constant.IsFloat && !isHexInt(constant.Text) && strings.ContainsAny(constant.Text, ".eEpP"):
       +
       +        case constant.IsFloat &&
       +                !isHexInt(constant.Text) && !isRuneInt(constant.Text) &&
       +                strings.ContainsAny(constant.Text, ".eEpP"):
                        return reflect.ValueOf(constant.Float64)
       +
                case constant.IsInt:
                        n := int(constant.Int64)
                        if int64(n) != constant.Int64 {
                                s.errorf("%s overflows int", constant.Text)
                        }
                        return reflect.ValueOf(n)
       +
                case constant.IsUint:
                        s.errorf("%s overflows int", constant.Text)
                }
                return zero
        }
        
       +func isRuneInt(s string) bool {
       +        return len(s) > 0 && s[0] == '\''
       +}
       +
        func isHexInt(s string) bool {
                return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP")
        }
   DIR diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
       @@ -354,6 +354,12 @@ var execTests = []execTest{
                {"field on interface", "{{.foo}}", "<no value>", nil, true},
                {"field on parenthesized interface", "{{(.).foo}}", "<no value>", nil, true},
        
       +        // Issue 31810: Parenthesized first element of pipeline with arguments.
       +        // See also TestIssue31810.
       +        {"unparenthesized non-function", "{{1 2}}", "", nil, false},
       +        {"parenthesized non-function", "{{(1) 2}}", "", nil, false},
       +        {"parenthesized non-function with no args", "{{(1)}}", "1", nil, true}, // This is fine.
       +
                // Method calls.
                {".Method0", "-{{.Method0}}-", "-M0-", tVal, true},
                {".Method1(1234)", "-{{.Method1 1234}}-", "-1234-", tVal, true},
       @@ -498,6 +504,7 @@ var execTests = []execTest{
                {"map MUI64S", "{{index .MUI64S 3}}", "ui643", tVal, true},
                {"map MI8S", "{{index .MI8S 3}}", "i83", tVal, true},
                {"map MUI8S", "{{index .MUI8S 2}}", "u82", tVal, true},
       +        {"index of an interface field", "{{index .Empty3 0}}", "7", tVal, true},
        
                // Slicing.
                {"slice[:]", "{{slice .SI}}", "[3 4 5]", tVal, true},
       @@ -523,12 +530,14 @@ var execTests = []execTest{
                {"string[1:2]", "{{slice .S 1 2}}", "y", tVal, true},
                {"out of range", "{{slice .S 1 5}}", "", tVal, false},
                {"3-index slice of string", "{{slice .S 1 2 2}}", "", tVal, false},
       +        {"slice of an interface field", "{{slice .Empty3 0 1}}", "[7]", tVal, true},
        
                // Len.
                {"slice", "{{len .SI}}", "3", tVal, true},
                {"map", "{{len .MSI }}", "3", tVal, true},
                {"len of int", "{{len 3}}", "", tVal, false},
                {"len of nothing", "{{len .Empty0}}", "", tVal, false},
       +        {"len of an interface field", "{{len .Empty3}}", "2", tVal, true},
        
                // With.
                {"with true", "{{with true}}{{.}}{{end}}", "true", tVal, true},
       @@ -665,6 +674,12 @@ var execTests = []execTest{
                {"bug17c", "{{len .NonEmptyInterfacePtS}}", "2", tVal, true},
                {"bug17d", "{{index .NonEmptyInterfacePtS 0}}", "a", tVal, true},
                {"bug17e", "{{range .NonEmptyInterfacePtS}}-{{.}}-{{end}}", "-a--b-", tVal, true},
       +
       +        // More variadic function corner cases. Some runes would get evaluated
       +        // as constant floats instead of ints. Issue 34483.
       +        {"bug18a", "{{eq . '.'}}", "true", '.', true},
       +        {"bug18b", "{{eq . 'e'}}", "true", 'e', true},
       +        {"bug18c", "{{eq . 'P'}}", "true", 'P', true},
        }
        
        func zeroArgs() string {
       @@ -898,7 +913,9 @@ func TestJSEscaping(t *testing.T) {
                        {`Go "jump" \`, `Go \"jump\" \\`},
                        {`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`},
                        {"unprintable \uFDFF", `unprintable \uFDFF`},
       -                {`<html>`, `\x3Chtml\x3E`},
       +                {`<html>`, `\u003Chtml\u003E`},
       +                {`no = in attributes`, `no \u003D in attributes`},
       +                {`&#x27; does not become HTML entity`, `\u0026#x27; does not become HTML entity`},
                }
                for _, tc := range testCases {
                        s := JSEscapeString(tc.in)
       @@ -1158,19 +1175,41 @@ var cmpTests = []cmpTest{
                {"ge .Uthree .NegOne", "true", true},
                {"eq (index `x` 0) 'x'", "true", true}, // The example that triggered this rule.
                {"eq (index `x` 0) 'y'", "false", true},
       +        {"eq .V1 .V2", "true", true},
       +        {"eq .Ptr .Ptr", "true", true},
       +        {"eq .Ptr .NilPtr", "false", true},
       +        {"eq .NilPtr .NilPtr", "true", true},
       +        {"eq .Iface1 .Iface1", "true", true},
       +        {"eq .Iface1 .Iface2", "false", true},
       +        {"eq .Iface2 .Iface2", "true", true},
                // 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 `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.
        }
        
        func TestComparison(t *testing.T) {
                b := new(bytes.Buffer)
                var cmpStruct = struct {
       -                Uthree, Ufour uint
       -                NegOne, Three int
       -        }{3, 4, -1, 3}
       +                Uthree, Ufour  uint
       +                NegOne, Three  int
       +                Ptr, NilPtr    *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,
       +        }
                for _, test := range cmpTests {
                        text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
                        tmpl, err := New("empty").Parse(text)
       @@ -1622,3 +1661,41 @@ 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
       +        const text = "{{ (.)  }}"
       +        tmpl, err := New("").Parse(text)
       +        if err != nil {
       +                t.Error(err)
       +        }
       +        err = tmpl.Execute(&b, "result")
       +        if err != nil {
       +                t.Error(err)
       +        }
       +        if b.String() != "result" {
       +                t.Errorf("%s got %q, expected %q", text, b.String(), "result")
       +        }
       +
       +        // Even a plain function fails - need to use call.
       +        f := func() string { return "result" }
       +        b.Reset()
       +        err = tmpl.Execute(&b, f)
       +        if err == nil {
       +                t.Error("expected error with no call, got none")
       +        }
       +
       +        // Works if the function is explicitly called.
       +        const textCall = "{{ (call .)  }}"
       +        tmpl, err = New("").Parse(textCall)
       +        b.Reset()
       +        err = tmpl.Execute(&b, f)
       +        if err != nil {
       +                t.Error(err)
       +        }
       +        if b.String() != "result" {
       +                t.Errorf("%s got %q, expected %q", textCall, b.String(), "result")
       +        }
       +}
   DIR diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go
       @@ -12,6 +12,7 @@ import (
                "net/url"
                "reflect"
                "strings"
       +        "sync"
                "unicode"
                "unicode/utf8"
        )
       @@ -29,31 +30,49 @@ import (
        // type can return interface{} or reflect.Value.
        type FuncMap map[string]interface{}
        
       -var builtins = FuncMap{
       -        "and":      and,
       -        "call":     call,
       -        "html":     HTMLEscaper,
       -        "index":    index,
       -        "slice":    slice,
       -        "js":       JSEscaper,
       -        "len":      length,
       -        "not":      not,
       -        "or":       or,
       -        "print":    fmt.Sprint,
       -        "printf":   fmt.Sprintf,
       -        "println":  fmt.Sprintln,
       -        "urlquery": URLQueryEscaper,
       -
       -        // Comparisons
       -        "eq": eq, // ==
       -        "ge": ge, // >=
       -        "gt": gt, // >
       -        "le": le, // <=
       -        "lt": lt, // <
       -        "ne": ne, // !=
       -}
       -
       -var builtinFuncs = createValueFuncs(builtins)
       +// builtins returns the FuncMap.
       +// It is not a global variable so the linker can dead code eliminate
       +// more when this isn't called. See golang.org/issue/36021.
       +// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
       +func builtins() FuncMap {
       +        return FuncMap{
       +                "and":      and,
       +                "call":     call,
       +                "html":     HTMLEscaper,
       +                "index":    index,
       +                "slice":    slice,
       +                "js":       JSEscaper,
       +                "len":      length,
       +                "not":      not,
       +                "or":       or,
       +                "print":    fmt.Sprint,
       +                "printf":   fmt.Sprintf,
       +                "println":  fmt.Sprintln,
       +                "urlquery": URLQueryEscaper,
       +
       +                // Comparisons
       +                "eq": eq, // ==
       +                "ge": ge, // >=
       +                "gt": gt, // >
       +                "le": le, // <=
       +                "lt": lt, // <
       +                "ne": ne, // !=
       +        }
       +}
       +
       +var builtinFuncsOnce struct {
       +        sync.Once
       +        v map[string]reflect.Value
       +}
       +
       +// builtinFuncsOnce lazily computes & caches the builtinFuncs map.
       +// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
       +func builtinFuncs() map[string]reflect.Value {
       +        builtinFuncsOnce.Do(func() {
       +                builtinFuncsOnce.v = createValueFuncs(builtins())
       +        })
       +        return builtinFuncsOnce.v
       +}
        
        // createValueFuncs turns a FuncMap into a map[string]reflect.Value
        func createValueFuncs(funcMap FuncMap) map[string]reflect.Value {
       @@ -125,7 +144,7 @@ func findFunction(name string, tmpl *Template) (reflect.Value, bool) {
                                return fn, true
                        }
                }
       -        if fn := builtinFuncs[name]; fn.IsValid() {
       +        if fn := builtinFuncs()[name]; fn.IsValid() {
                        return fn, true
                }
                return reflect.Value{}, false
       @@ -185,41 +204,41 @@ func indexArg(index reflect.Value, cap int) (int, error) {
        // arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
        // indexed item must be a map, slice, or array.
        func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
       -        v := indirectInterface(item)
       -        if !v.IsValid() {
       +        item = indirectInterface(item)
       +        if !item.IsValid() {
                        return reflect.Value{}, fmt.Errorf("index of untyped nil")
                }
       -        for _, i := range indexes {
       -                index := indirectInterface(i)
       +        for _, index := range indexes {
       +                index = indirectInterface(index)
                        var isNil bool
       -                if v, isNil = indirect(v); isNil {
       +                if item, isNil = indirect(item); isNil {
                                return reflect.Value{}, fmt.Errorf("index of nil pointer")
                        }
       -                switch v.Kind() {
       +                switch item.Kind() {
                        case reflect.Array, reflect.Slice, reflect.String:
       -                        x, err := indexArg(index, v.Len())
       +                        x, err := indexArg(index, item.Len())
                                if err != nil {
                                        return reflect.Value{}, err
                                }
       -                        v = v.Index(x)
       +                        item = item.Index(x)
                        case reflect.Map:
       -                        index, err := prepareArg(index, v.Type().Key())
       +                        index, err := prepareArg(index, item.Type().Key())
                                if err != nil {
                                        return reflect.Value{}, err
                                }
       -                        if x := v.MapIndex(index); x.IsValid() {
       -                                v = x
       +                        if x := item.MapIndex(index); x.IsValid() {
       +                                item = x
                                } else {
       -                                v = reflect.Zero(v.Type().Elem())
       +                                item = reflect.Zero(item.Type().Elem())
                                }
                        case reflect.Invalid:
       -                        // the loop holds invariant: v.IsValid()
       +                        // the loop holds invariant: item.IsValid()
                                panic("unreachable")
                        default:
       -                        return reflect.Value{}, fmt.Errorf("can't index item of type %s", v.Type())
       +                        return reflect.Value{}, fmt.Errorf("can't index item of type %s", item.Type())
                        }
                }
       -        return v, nil
       +        return item, nil
        }
        
        // Slicing.
       @@ -229,29 +248,27 @@ func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) 
        // is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first
        // argument must be a string, slice, or array.
        func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
       -        var (
       -                cap int
       -                v   = indirectInterface(item)
       -        )
       -        if !v.IsValid() {
       +        item = indirectInterface(item)
       +        if !item.IsValid() {
                        return reflect.Value{}, fmt.Errorf("slice of untyped nil")
                }
                if len(indexes) > 3 {
                        return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
                }
       -        switch v.Kind() {
       +        var cap int
       +        switch item.Kind() {
                case reflect.String:
                        if len(indexes) == 3 {
                                return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string")
                        }
       -                cap = v.Len()
       +                cap = item.Len()
                case reflect.Array, reflect.Slice:
       -                cap = v.Cap()
       +                cap = item.Cap()
                default:
       -                return reflect.Value{}, fmt.Errorf("can't slice item of type %s", v.Type())
       +                return reflect.Value{}, fmt.Errorf("can't slice item of type %s", item.Type())
                }
                // set default values for cases item[:], item[i:].
       -        idx := [3]int{0, v.Len()}
       +        idx := [3]int{0, item.Len()}
                for i, index := range indexes {
                        x, err := indexArg(index, cap)
                        if err != nil {
       @@ -276,20 +293,16 @@ func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) 
        // Length
        
        // length returns the length of the item, with an error if it has no defined length.
       -func length(item interface{}) (int, error) {
       -        v := reflect.ValueOf(item)
       -        if !v.IsValid() {
       -                return 0, fmt.Errorf("len of untyped nil")
       -        }
       -        v, isNil := indirect(v)
       +func length(item reflect.Value) (int, error) {
       +        item, isNil := indirect(item)
                if isNil {
                        return 0, fmt.Errorf("len of nil pointer")
                }
       -        switch v.Kind() {
       +        switch item.Kind() {
                case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
       -                return v.Len(), nil
       +                return item.Len(), nil
                }
       -        return 0, fmt.Errorf("len of type %s", v.Type())
       +        return 0, fmt.Errorf("len of type %s", item.Type())
        }
        
        // Function invocation
       @@ -297,11 +310,11 @@ func length(item interface{}) (int, error) {
        // call returns the result of evaluating the first argument as a function.
        // The function must return 1 result, or 2 results, the second of which is an error.
        func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
       -        v := indirectInterface(fn)
       -        if !v.IsValid() {
       +        fn = indirectInterface(fn)
       +        if !fn.IsValid() {
                        return reflect.Value{}, fmt.Errorf("call of nil")
                }
       -        typ := v.Type()
       +        typ := fn.Type()
                if typ.Kind() != reflect.Func {
                        return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
                }
       @@ -322,7 +335,7 @@ func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
                }
                argv := make([]reflect.Value, len(args))
                for i, arg := range args {
       -                value := indirectInterface(arg)
       +                arg = indirectInterface(arg)
                        // Compute the expected type. Clumsy because of variadics.
                        argType := dddType
                        if !typ.IsVariadic() || i < numIn-1 {
       @@ -330,11 +343,11 @@ func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
                        }
        
                        var err error
       -                if argv[i], err = prepareArg(value, argType); err != nil {
       +                if argv[i], err = prepareArg(arg, argType); err != nil {
                                return reflect.Value{}, fmt.Errorf("arg %d: %s", i, err)
                        }
                }
       -        return safeCall(v, argv)
       +        return safeCall(fn, argv)
        }
        
        // safeCall runs fun.Call(args), and returns the resulting value and error, if
       @@ -440,47 +453,53 @@ func basicKind(v reflect.Value) (kind, error) {
        
        // eq evaluates the comparison a == b || a == c || ...
        func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
       -        v1 := indirectInterface(arg1)
       -        k1, err := basicKind(v1)
       -        if err != nil {
       -                return false, err
       +        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
                }
       +        k1, _ := basicKind(arg1)
                for _, arg := range arg2 {
       -                v2 := indirectInterface(arg)
       -                k2, err := basicKind(v2)
       -                if err != nil {
       -                        return false, err
       -                }
       +                arg = indirectInterface(arg)
       +                k2, _ := basicKind(arg)
                        truth := false
                        if k1 != k2 {
                                // Special case: Can compare integer values regardless of type's sign.
                                switch {
                                case k1 == intKind && k2 == uintKind:
       -                                truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
       +                                truth = arg1.Int() >= 0 && uint64(arg1.Int()) == arg.Uint()
                                case k1 == uintKind && k2 == intKind:
       -                                truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
       +                                truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
                                default:
                                        return false, errBadComparison
                                }
                        } else {
                                switch k1 {
                                case boolKind:
       -                                truth = v1.Bool() == v2.Bool()
       +                                truth = arg1.Bool() == arg.Bool()
                                case complexKind:
       -                                truth = v1.Complex() == v2.Complex()
       +                                truth = arg1.Complex() == arg.Complex()
                                case floatKind:
       -                                truth = v1.Float() == v2.Float()
       +                                truth = arg1.Float() == arg.Float()
                                case intKind:
       -                                truth = v1.Int() == v2.Int()
       +                                truth = arg1.Int() == arg.Int()
                                case stringKind:
       -                                truth = v1.String() == v2.String()
       +                                truth = arg1.String() == arg.String()
                                case uintKind:
       -                                truth = v1.Uint() == v2.Uint()
       +                                truth = arg1.Uint() == arg.Uint()
                                default:
       -                                panic("invalid kind")
       +                                if arg == zero {
       +                                        truth = arg1 == arg
       +                                } else {
       +                                        if t2 := arg.Type(); !t2.Comparable() {
       +                                                return false, fmt.Errorf("uncomparable type %s: %v", t2, arg)
       +                                        }
       +                                        truth = arg1.Interface() == arg.Interface()
       +                                }
                                }
                        }
                        if truth {
       @@ -499,13 +518,13 @@ func ne(arg1, arg2 reflect.Value) (bool, error) {
        
        // lt evaluates the comparison a < b.
        func lt(arg1, arg2 reflect.Value) (bool, error) {
       -        v1 := indirectInterface(arg1)
       -        k1, err := basicKind(v1)
       +        arg1 = indirectInterface(arg1)
       +        k1, err := basicKind(arg1)
                if err != nil {
                        return false, err
                }
       -        v2 := indirectInterface(arg2)
       -        k2, err := basicKind(v2)
       +        arg2 = indirectInterface(arg2)
       +        k2, err := basicKind(arg2)
                if err != nil {
                        return false, err
                }
       @@ -514,9 +533,9 @@ func lt(arg1, arg2 reflect.Value) (bool, error) {
                        // Special case: Can compare integer values regardless of type's sign.
                        switch {
                        case k1 == intKind && k2 == uintKind:
       -                        truth = v1.Int() < 0 || uint64(v1.Int()) < v2.Uint()
       +                        truth = arg1.Int() < 0 || uint64(arg1.Int()) < arg2.Uint()
                        case k1 == uintKind && k2 == intKind:
       -                        truth = v2.Int() >= 0 && v1.Uint() < uint64(v2.Int())
       +                        truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int())
                        default:
                                return false, errBadComparison
                        }
       @@ -525,13 +544,13 @@ func lt(arg1, arg2 reflect.Value) (bool, error) {
                        case boolKind, complexKind:
                                return false, errBadComparisonType
                        case floatKind:
       -                        truth = v1.Float() < v2.Float()
       +                        truth = arg1.Float() < arg2.Float()
                        case intKind:
       -                        truth = v1.Int() < v2.Int()
       +                        truth = arg1.Int() < arg2.Int()
                        case stringKind:
       -                        truth = v1.String() < v2.String()
       +                        truth = arg1.String() < arg2.String()
                        case uintKind:
       -                        truth = v1.Uint() < v2.Uint()
       +                        truth = arg1.Uint() < arg2.Uint()
                        default:
                                panic("invalid kind")
                        }
       @@ -634,8 +653,10 @@ var (
                jsBackslash = []byte(`\\`)
                jsApos      = []byte(`\'`)
                jsQuot      = []byte(`\"`)
       -        jsLt        = []byte(`\x3C`)
       -        jsGt        = []byte(`\x3E`)
       +        jsLt        = []byte(`\u003C`)
       +        jsGt        = []byte(`\u003E`)
       +        jsAmp       = []byte(`\u0026`)
       +        jsEq        = []byte(`\u003D`)
        )
        
        // JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
       @@ -664,6 +685,10 @@ func JSEscape(w io.Writer, b []byte) {
                                        w.Write(jsLt)
                                case '>':
                                        w.Write(jsGt)
       +                        case '&':
       +                                w.Write(jsAmp)
       +                        case '=':
       +                                w.Write(jsEq)
                                default:
                                        w.Write(jsLowUni)
                                        t, b := c>>4, c&0x0f
       @@ -698,7 +723,7 @@ func JSEscapeString(s string) string {
        
        func jsIsSpecial(r rune) bool {
                switch r {
       -        case '\\', '\'', '"', '<', '>':
       +        case '\\', '\'', '"', '<', '>', '&', '=':
                        return true
                }
                return r < ' ' || utf8.RuneSelf <= r
   DIR diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
       @@ -30,7 +30,7 @@ package is auto generated.
        */
        
        // Export it so we can populate Hugo's func map with it, which makes it faster.
       -var GoFuncs = builtinFuncs
       +var GoFuncs = builtinFuncs()
        
        // Preparer prepares the template before execution.
        type Preparer interface {
   DIR diff --git a/tpl/internal/go_templates/texttemplate/multi_test.go b/tpl/internal/go_templates/texttemplate/multi_test.go
       @@ -244,7 +244,7 @@ func TestAddParseTree(t *testing.T) {
                        t.Fatal(err)
                }
                // Add a new parse tree.
       -        tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
       +        tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins())
                if err != nil {
                        t.Fatal(err)
                }
   DIR diff --git a/tpl/internal/go_templates/texttemplate/parse/lex.go b/tpl/internal/go_templates/texttemplate/parse/lex.go
       @@ -411,7 +411,6 @@ func lexInsideAction(l *lexer) stateFn {
                        }
                case r <= unicode.MaxASCII && unicode.IsPrint(r):
                        l.emit(itemChar)
       -                return lexInsideAction
                default:
                        return l.errorf("unrecognized character in action: %#U", r)
                }
   DIR diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go
       @@ -7,7 +7,6 @@
        package parse
        
        import (
       -        "bytes"
                "fmt"
                "strconv"
                "strings"
       @@ -29,6 +28,8 @@ type Node interface {
                // tree returns the containing *Tree.
                // It is unexported so all implementations of Node are in this package.
                tree() *Tree
       +        // writeTo writes the String output to the builder.
       +        writeTo(*strings.Builder)
        }
        
        // NodeType identifies the type of a parse tree node.
       @@ -94,11 +95,15 @@ func (l *ListNode) tree() *Tree {
        }
        
        func (l *ListNode) String() string {
       -        b := new(bytes.Buffer)
       +        var sb strings.Builder
       +        l.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (l *ListNode) writeTo(sb *strings.Builder) {
                for _, n := range l.Nodes {
       -                fmt.Fprint(b, n)
       +                n.writeTo(sb)
                }
       -        return b.String()
        }
        
        func (l *ListNode) CopyList() *ListNode {
       @@ -132,6 +137,10 @@ func (t *TextNode) String() string {
                return fmt.Sprintf(textFormat, t.Text)
        }
        
       +func (t *TextNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(t.String())
       +}
       +
        func (t *TextNode) tree() *Tree {
                return t.tr
        }
       @@ -160,23 +169,27 @@ func (p *PipeNode) append(command *CommandNode) {
        }
        
        func (p *PipeNode) String() string {
       -        s := ""
       +        var sb strings.Builder
       +        p.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (p *PipeNode) writeTo(sb *strings.Builder) {
                if len(p.Decl) > 0 {
                        for i, v := range p.Decl {
                                if i > 0 {
       -                                s += ", "
       +                                sb.WriteString(", ")
                                }
       -                        s += v.String()
       +                        v.writeTo(sb)
                        }
       -                s += " := "
       +                sb.WriteString(" := ")
                }
                for i, c := range p.Cmds {
                        if i > 0 {
       -                        s += " | "
       +                        sb.WriteString(" | ")
                        }
       -                s += c.String()
       +                c.writeTo(sb)
                }
       -        return s
        }
        
        func (p *PipeNode) tree() *Tree {
       @@ -187,9 +200,9 @@ func (p *PipeNode) CopyPipe() *PipeNode {
                if p == nil {
                        return p
                }
       -        var vars []*VariableNode
       -        for _, d := range p.Decl {
       -                vars = append(vars, d.Copy().(*VariableNode))
       +        vars := make([]*VariableNode, len(p.Decl))
       +        for i, d := range p.Decl {
       +                vars[i] = d.Copy().(*VariableNode)
                }
                n := p.tr.newPipeline(p.Pos, p.Line, vars)
                n.IsAssign = p.IsAssign
       @@ -219,8 +232,15 @@ func (t *Tree) newAction(pos Pos, line int, pipe *PipeNode) *ActionNode {
        }
        
        func (a *ActionNode) String() string {
       -        return fmt.Sprintf("{{%s}}", a.Pipe)
       +        var sb strings.Builder
       +        a.writeTo(&sb)
       +        return sb.String()
       +}
        
       +func (a *ActionNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString("{{")
       +        a.Pipe.writeTo(sb)
       +        sb.WriteString("}}")
        }
        
        func (a *ActionNode) tree() *Tree {
       @@ -249,18 +269,24 @@ func (c *CommandNode) append(arg Node) {
        }
        
        func (c *CommandNode) String() string {
       -        s := ""
       +        var sb strings.Builder
       +        c.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (c *CommandNode) writeTo(sb *strings.Builder) {
                for i, arg := range c.Args {
                        if i > 0 {
       -                        s += " "
       +                        sb.WriteByte(' ')
                        }
                        if arg, ok := arg.(*PipeNode); ok {
       -                        s += "(" + arg.String() + ")"
       +                        sb.WriteByte('(')
       +                        arg.writeTo(sb)
       +                        sb.WriteByte(')')
                                continue
                        }
       -                s += arg.String()
       +                arg.writeTo(sb)
                }
       -        return s
        }
        
        func (c *CommandNode) tree() *Tree {
       @@ -311,6 +337,10 @@ func (i *IdentifierNode) String() string {
                return i.Ident
        }
        
       +func (i *IdentifierNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(i.String())
       +}
       +
        func (i *IdentifierNode) tree() *Tree {
                return i.tr
        }
       @@ -333,14 +363,18 @@ func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
        }
        
        func (v *VariableNode) String() string {
       -        s := ""
       +        var sb strings.Builder
       +        v.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (v *VariableNode) writeTo(sb *strings.Builder) {
                for i, id := range v.Ident {
                        if i > 0 {
       -                        s += "."
       +                        sb.WriteByte('.')
                        }
       -                s += id
       +                sb.WriteString(id)
                }
       -        return s
        }
        
        func (v *VariableNode) tree() *Tree {
       @@ -373,6 +407,10 @@ func (d *DotNode) String() string {
                return "."
        }
        
       +func (d *DotNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(d.String())
       +}
       +
        func (d *DotNode) tree() *Tree {
                return d.tr
        }
       @@ -403,6 +441,10 @@ func (n *NilNode) String() string {
                return "nil"
        }
        
       +func (n *NilNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(n.String())
       +}
       +
        func (n *NilNode) tree() *Tree {
                return n.tr
        }
       @@ -426,11 +468,16 @@ func (t *Tree) newField(pos Pos, ident string) *FieldNode {
        }
        
        func (f *FieldNode) String() string {
       -        s := ""
       +        var sb strings.Builder
       +        f.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (f *FieldNode) writeTo(sb *strings.Builder) {
                for _, id := range f.Ident {
       -                s += "." + id
       +                sb.WriteByte('.')
       +                sb.WriteString(id)
                }
       -        return s
        }
        
        func (f *FieldNode) tree() *Tree {
       @@ -469,14 +516,23 @@ func (c *ChainNode) Add(field string) {
        }
        
        func (c *ChainNode) String() string {
       -        s := c.Node.String()
       +        var sb strings.Builder
       +        c.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (c *ChainNode) writeTo(sb *strings.Builder) {
                if _, ok := c.Node.(*PipeNode); ok {
       -                s = "(" + s + ")"
       +                sb.WriteByte('(')
       +                c.Node.writeTo(sb)
       +                sb.WriteByte(')')
       +        } else {
       +                c.Node.writeTo(sb)
                }
                for _, field := range c.Field {
       -                s += "." + field
       +                sb.WriteByte('.')
       +                sb.WriteString(field)
                }
       -        return s
        }
        
        func (c *ChainNode) tree() *Tree {
       @@ -506,6 +562,10 @@ func (b *BoolNode) String() string {
                return "false"
        }
        
       +func (b *BoolNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(b.String())
       +}
       +
        func (b *BoolNode) tree() *Tree {
                return b.tr
        }
       @@ -639,6 +699,10 @@ func (n *NumberNode) String() string {
                return n.Text
        }
        
       +func (n *NumberNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(n.String())
       +}
       +
        func (n *NumberNode) tree() *Tree {
                return n.tr
        }
       @@ -666,6 +730,10 @@ func (s *StringNode) String() string {
                return s.Quoted
        }
        
       +func (s *StringNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(s.String())
       +}
       +
        func (s *StringNode) tree() *Tree {
                return s.tr
        }
       @@ -690,6 +758,10 @@ func (e *endNode) String() string {
                return "{{end}}"
        }
        
       +func (e *endNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(e.String())
       +}
       +
        func (e *endNode) tree() *Tree {
                return e.tr
        }
       @@ -718,6 +790,10 @@ func (e *elseNode) String() string {
                return "{{else}}"
        }
        
       +func (e *elseNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString(e.String())
       +}
       +
        func (e *elseNode) tree() *Tree {
                return e.tr
        }
       @@ -738,6 +814,12 @@ type BranchNode struct {
        }
        
        func (b *BranchNode) String() string {
       +        var sb strings.Builder
       +        b.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (b *BranchNode) writeTo(sb *strings.Builder) {
                name := ""
                switch b.NodeType {
                case NodeIf:
       @@ -749,10 +831,17 @@ func (b *BranchNode) String() string {
                default:
                        panic("unknown branch type")
                }
       +        sb.WriteString("{{")
       +        sb.WriteString(name)
       +        sb.WriteByte(' ')
       +        b.Pipe.writeTo(sb)
       +        sb.WriteString("}}")
       +        b.List.writeTo(sb)
                if b.ElseList != nil {
       -                return fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}", name, b.Pipe, b.List, b.ElseList)
       +                sb.WriteString("{{else}}")
       +                b.ElseList.writeTo(sb)
                }
       -        return fmt.Sprintf("{{%s %s}}%s{{end}}", name, b.Pipe, b.List)
       +        sb.WriteString("{{end}}")
        }
        
        func (b *BranchNode) tree() *Tree {
       @@ -826,10 +915,19 @@ func (t *Tree) newTemplate(pos Pos, line int, name string, pipe *PipeNode) *Temp
        }
        
        func (t *TemplateNode) String() string {
       -        if t.Pipe == nil {
       -                return fmt.Sprintf("{{template %q}}", t.Name)
       +        var sb strings.Builder
       +        t.writeTo(&sb)
       +        return sb.String()
       +}
       +
       +func (t *TemplateNode) writeTo(sb *strings.Builder) {
       +        sb.WriteString("{{template ")
       +        sb.WriteString(strconv.Quote(t.Name))
       +        if t.Pipe != nil {
       +                sb.WriteByte(' ')
       +                t.Pipe.writeTo(sb)
                }
       -        return fmt.Sprintf("{{template %q %s}}", t.Name, t.Pipe)
       +        sb.WriteString("}}")
        }
        
        func (t *TemplateNode) tree() *Tree {
   DIR diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
       @@ -108,13 +108,8 @@ func (t *Tree) nextNonSpace() (token item) {
        }
        
        // peekNonSpace returns but does not consume the next non-space token.
       -func (t *Tree) peekNonSpace() (token item) {
       -        for {
       -                token = t.next()
       -                if token.typ != itemSpace {
       -                        break
       -                }
       -        }
       +func (t *Tree) peekNonSpace() item {
       +        token := t.nextNonSpace()
                t.backup()
                return token
        }
   DIR diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
       @@ -306,7 +306,8 @@ var parseTests = []parseTest{
        }
        
        var builtins = map[string]interface{}{
       -        "printf": fmt.Sprintf,
       +        "printf":   fmt.Sprintf,
       +        "contains": strings.Contains,
        }
        
        func testParse(doCopy bool, t *testing.T) {
       @@ -555,3 +556,52 @@ func BenchmarkParseLarge(b *testing.B) {
                        }
                }
        }
       +
       +var sinkv, sinkl string
       +
       +func BenchmarkVariableString(b *testing.B) {
       +        v := &VariableNode{
       +                Ident: []string{"$", "A", "BB", "CCC", "THIS_IS_THE_VARIABLE_BEING_PROCESSED"},
       +        }
       +        b.ResetTimer()
       +        b.ReportAllocs()
       +        for i := 0; i < b.N; i++ {
       +                sinkv = v.String()
       +        }
       +        if sinkv == "" {
       +                b.Fatal("Benchmark was not run")
       +        }
       +}
       +
       +func BenchmarkListString(b *testing.B) {
       +        text := `
       +{{(printf .Field1.Field2.Field3).Value}}
       +{{$x := (printf .Field1.Field2.Field3).Value}}
       +{{$y := (printf $x.Field1.Field2.Field3).Value}}
       +{{$z := $y.Field1.Field2.Field3}}
       +{{if contains $y $z}}
       +        {{printf "%q" $y}}
       +{{else}}
       +        {{printf "%q" $x}}
       +{{end}}
       +{{with $z.Field1 | contains "boring"}}
       +        {{printf "%q" . | printf "%s"}}
       +{{else}}
       +        {{printf "%d %d %d" 11 11 11}}
       +        {{printf "%d %d %s" 22 22 $x.Field1.Field2.Field3 | printf "%s"}}
       +        {{printf "%v" (contains $z.Field1.Field2 $y)}}
       +{{end}}
       +`
       +        tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
       +        if err != nil {
       +                b.Fatal(err)
       +        }
       +        b.ResetTimer()
       +        b.ReportAllocs()
       +        for i := 0; i < b.N; i++ {
       +                sinkl = tree.Root.String()
       +        }
       +        if sinkl == "" {
       +                b.Fatal("Benchmark was not run")
       +        }
       +}
   DIR diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go
       @@ -110,20 +110,21 @@ func (t *Template) Clone() (*Template, error) {
        
        // copy returns a shallow copy of t, with common set to the argument.
        func (t *Template) copy(c *common) *Template {
       -        nt := New(t.name)
       -        nt.Tree = t.Tree
       -        nt.common = c
       -        nt.leftDelim = t.leftDelim
       -        nt.rightDelim = t.rightDelim
       -        return nt
       +        return &Template{
       +                name:       t.name,
       +                Tree:       t.Tree,
       +                common:     c,
       +                leftDelim:  t.leftDelim,
       +                rightDelim: t.rightDelim,
       +        }
        }
        
       -// AddParseTree adds parse tree for template with given name and associates it with t.
       -// If the template does not already exist, it will create a new one.
       -// If the template does exist, it will be replaced.
       +// AddParseTree associates the argument parse tree with the template t, giving
       +// it the specified name. If the template has not been defined, this tree becomes
       +// its definition. If it has been defined and already has that name, the existing
       +// definition is replaced; otherwise a new template is created, defined, and returned.
        func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
                t.init()
       -        // If the name is the name of this template, overwrite this template.
                nt := t
                if name != t.name {
                        nt = t.New(name)
       @@ -197,7 +198,7 @@ func (t *Template) Lookup(name string) *Template {
        func (t *Template) Parse(text string) (*Template, error) {
                t.init()
                t.muFuncs.RLock()
       -        trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins)
       +        trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins())
                t.muFuncs.RUnlock()
                if err != nil {
                        return nil, err