Fix Go template script escaping
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 23 May 2020 13:32:27 +0000 (15:32 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 23 May 2020 20:00:34 +0000 (22:00 +0200)
Fixes #6695

27 files changed:
hugolib/template_test.go
scripts/fork_go_templates/main.go
tpl/internal/go_templates/cfg/cfg.go [new file with mode: 0644]
tpl/internal/go_templates/fmtsort/sort.go
tpl/internal/go_templates/fmtsort/sort_test.go
tpl/internal/go_templates/htmltemplate/content_test.go
tpl/internal/go_templates/htmltemplate/doc.go
tpl/internal/go_templates/htmltemplate/escape_test.go
tpl/internal/go_templates/htmltemplate/example_test.go
tpl/internal/go_templates/htmltemplate/js.go
tpl/internal/go_templates/htmltemplate/js_test.go
tpl/internal/go_templates/htmltemplate/template_test.go
tpl/internal/go_templates/testenv/testenv.go [new file with mode: 0644]
tpl/internal/go_templates/testenv/testenv_cgo.go [new file with mode: 0644]
tpl/internal/go_templates/testenv/testenv_notwin.go [new file with mode: 0644]
tpl/internal/go_templates/testenv/testenv_windows.go [new file with mode: 0644]
tpl/internal/go_templates/texttemplate/doc.go
tpl/internal/go_templates/texttemplate/exec.go
tpl/internal/go_templates/texttemplate/exec_test.go
tpl/internal/go_templates/texttemplate/funcs.go
tpl/internal/go_templates/texttemplate/hugo_template.go
tpl/internal/go_templates/texttemplate/multi_test.go
tpl/internal/go_templates/texttemplate/parse/lex.go
tpl/internal/go_templates/texttemplate/parse/node.go
tpl/internal/go_templates/texttemplate/parse/parse.go
tpl/internal/go_templates/texttemplate/parse/parse_test.go
tpl/internal/go_templates/texttemplate/template.go

index 9f04aabdd2974588320ff50b05cf4a091179ff8e..29993120d8ba2bdc67caa8930d6fae87786cc897 100644 (file)
@@ -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() {
index d9d056797524301f884d1ce9b1fab0ed9a10d287..04202b2544469f43e1ed4fba6e36f3f357d4311d 100644 (file)
@@ -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()
diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go
new file mode 100644 (file)
index 0000000..bdbe9df
--- /dev/null
@@ -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
+`
index 70a305a3a10330674e285fe34c54d5a084c5d305..b01229bd06aab68aeb70212c4ff0ad22d054edfc 100644 (file)
@@ -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,
index 601ec9d25980b2697542950adfa041176d1eb3d8..364c5bf6d27f45d51de62abf83a33be67944427d 100644 (file)
@@ -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",
        },
        {
index 2a1abfbfb22df6232508827ca85ffc7b85935d76..b5de701d350a55167d81c533b3d70b50ceba869e 100644 (file)
@@ -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`,
index 0a3004a9b1986d5c374a1733214782161372e9a7..b6a1504f85d70b0b3968f9fb2d5dcedb06621cdc 100644 (file)
@@ -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.
index 9e9db78003422d7b24b51dfd1157dafc03595e15..075db4e13714845ee2440d0760387a9b5575cb03 100644 (file)
@@ -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.
index a3e7910ee047d8546a34d18e6076d31439b26ce3..a93b8d2fbbdf52b857907eef37f8563390cd7011 100644 (file)
@@ -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
 
 }
index 57622d152e639caf8102593fe5f8e802b8ca403f..cfd413461f36096a5c8d211bf84730ef5182a137 100644 (file)
@@ -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
index 0a6f332a64ac7a751bde20f4007e1539e417d785..e15087f0f741c92bb441bd48390bbf0fc02806dd 100644 (file)
@@ -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" +
index 8adf3324b050cbb4c85fef6661609395a31242c2..589e6912a247c004b58345628121ddae13f8524e 100644 (file)
@@ -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
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
new file mode 100644 (file)
index 0000000..9004457
--- /dev/null
@@ -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
+}
diff --git a/tpl/internal/go_templates/testenv/testenv_cgo.go b/tpl/internal/go_templates/testenv/testenv_cgo.go
new file mode 100644 (file)
index 0000000..e3d4d16
--- /dev/null
@@ -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
+}
diff --git a/tpl/internal/go_templates/testenv/testenv_notwin.go b/tpl/internal/go_templates/testenv/testenv_notwin.go
new file mode 100644 (file)
index 0000000..ccb5d55
--- /dev/null
@@ -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, ""
+}
diff --git a/tpl/internal/go_templates/testenv/testenv_windows.go b/tpl/internal/go_templates/testenv/testenv_windows.go
new file mode 100644 (file)
index 0000000..eb8d6ac
--- /dev/null
@@ -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, ""
+}
index dbffaa4958001de16712cff31dc7a909f3181483..4b0efd2df87ad38012eb92aba3ca83563737b1f8 100644 (file)
@@ -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
 
index f400abd9cfe26736fa0ef1ab43adfeee43cb7cd4..879cd08846ea70d3baf5d2d3a3b2b1954df868d5 100644 (file)
@@ -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")
 }
index 504967db68a887f35068efc235d4cc310dddcc98..940a1de6a0543072bcc3a2e867d1cf0282bdf0ef 100644 (file)
@@ -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")
+       }
+}
index 248dbcf22ed99c098a7ad7081e7fc3f9f33fabe8..1b6940a84ae0d702f5775b8ca85efddd3b15afbb 100644 (file)
@@ -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
index 37fa969da92c939873b84c92247b967ec80f6e81..7cd6df0fb410d581c0ea35192348e13be3bbf331 100644 (file)
@@ -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 {
index e41bb9d8edc6b8576062492a006dca9f84f8ef64..7323be379515dc0b22421ec093f64083b311ae6c 100644 (file)
@@ -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)
        }
index 3d57708796ca795ef0c485ad5e630f3a8f5a09c1..30371f28626761633106b1dc4460833cff83a9a6 100644 (file)
@@ -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)
        }
index 1174a4b970673ae6e4936e1def036238a45d46a6..1c116ea6fab3dbf594306b9846dbf97f74715243 100644 (file)
@@ -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 {
index 7c35b0ff3d89169d7b249cc55003670b36644725..c9b80f4a24836846a54bba632c75c13fe9310169 100644 (file)
@@ -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
 }
index 5d3b59b63c852d50bdbd29de1d2eaaced0f9d2b9..79e7bb5ae5faeb0830a65f692c6cf31b8c9f7dcd 100644 (file)
@@ -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")
+       }
+}
index 4df8a9ed0abb01d46095db4e2d92eaf3e6b3e913..9c6ba6dfca089fcbb26a0ec7d4e43a2a81b5ea90 100644 (file)
@@ -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