mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
parent
c34bf48560
commit
6c3c6686f5
27 changed files with 1090 additions and 292 deletions
|
@ -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) {
|
func collectIdentities(set map[identity.Identity]bool, provider identity.Provider) {
|
||||||
if ids, ok := provider.(identity.IdentitiesProvider); ok {
|
if ids, ok := provider.(identity.IdentitiesProvider); ok {
|
||||||
for _, id := range ids.GetIdentities() {
|
for _, id := range ids.GetIdentities() {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// TODO(bep) git checkout tag
|
// 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 ...")
|
fmt.Println("Forking ...")
|
||||||
defer fmt.Println("Done ...")
|
defer fmt.Println("Done ...")
|
||||||
|
|
||||||
|
@ -55,6 +55,8 @@ var (
|
||||||
textTemplateReplacers = strings.NewReplacer(
|
textTemplateReplacers = strings.NewReplacer(
|
||||||
`"text/template/`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/`,
|
`"text/template/`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/`,
|
||||||
`"internal/fmtsort"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`,
|
`"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.
|
// Rename types and function that we want to overload.
|
||||||
"type state struct", "type stateOld struct",
|
"type state struct", "type stateOld struct",
|
||||||
"func (s *state) evalFunction", "func (s *state) evalFunctionOld",
|
"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) {",
|
"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(
|
htmlTemplateReplacers = strings.NewReplacer(
|
||||||
`. "html/template"`, `. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"`,
|
`. "html/template"`, `. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"`,
|
||||||
`"html/template"`, `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) {
|
goPackage{srcPkg: "internal/fmtsort", dstPkg: "fmtsort", rewriter: func(name string) {
|
||||||
rewrite(name, `"internal/fmtsort" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`)
|
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()
|
var fs = afero.NewOsFs()
|
||||||
|
|
64
tpl/internal/go_templates/cfg/cfg.go
Normal file
64
tpl/internal/go_templates/cfg/cfg.go
Normal file
|
@ -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
|
||||||
|
`
|
|
@ -53,12 +53,16 @@ func Sort(mapValue reflect.Value) *SortedMap {
|
||||||
if mapValue.Type().Kind() != reflect.Map {
|
if mapValue.Type().Kind() != reflect.Map {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
key := make([]reflect.Value, mapValue.Len())
|
// Note: this code is arranged to not panic even in the presence
|
||||||
value := make([]reflect.Value, len(key))
|
// 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()
|
iter := mapValue.MapRange()
|
||||||
for i := 0; iter.Next(); i++ {
|
for iter.Next() {
|
||||||
key[i] = iter.Key()
|
key = append(key, iter.Key())
|
||||||
value[i] = iter.Value()
|
value = append(value, iter.Value())
|
||||||
}
|
}
|
||||||
sorted := &SortedMap{
|
sorted := &SortedMap{
|
||||||
Key: key,
|
Key: key,
|
||||||
|
|
|
@ -119,7 +119,7 @@ var sortTests = []sortTest{
|
||||||
"PTR0:0 PTR1:1 PTR2:2",
|
"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",
|
"{3 4}:34 {7 1}:71 {7 2}:72",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,7 +21,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
htmltemplate.HTML(`Hello, <b>World</b> &tc!`),
|
htmltemplate.HTML(`Hello, <b>World</b> &tc!`),
|
||||||
htmltemplate.HTMLAttr(` dir="ltr"`),
|
htmltemplate.HTMLAttr(` dir="ltr"`),
|
||||||
htmltemplate.JS(`c && alert("Hello, World!");`),
|
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.URL(`greeting=H%69,&addressee=(World)`),
|
||||||
htmltemplate.Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
|
htmltemplate.Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
|
||||||
htmltemplate.URL(`,foo/,`),
|
htmltemplate.URL(`,foo/,`),
|
||||||
|
@ -73,7 +73,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
`Hello, <b>World</b> &tc!`,
|
`Hello, <b>World</b> &tc!`,
|
||||||
` dir="ltr"`,
|
` dir="ltr"`,
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
`Hello, World & O'Reilly\x21`,
|
`Hello, World & O'Reilly\u0021`,
|
||||||
`greeting=H%69,&addressee=(World)`,
|
`greeting=H%69,&addressee=(World)`,
|
||||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||||
`,foo/,`,
|
`,foo/,`,
|
||||||
|
@ -103,7 +103,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
`Hello, World &tc!`,
|
`Hello, World &tc!`,
|
||||||
` dir="ltr"`,
|
` dir="ltr"`,
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
`Hello, World & O'Reilly\x21`,
|
`Hello, World & O'Reilly\u0021`,
|
||||||
`greeting=H%69,&addressee=(World)`,
|
`greeting=H%69,&addressee=(World)`,
|
||||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||||
`,foo/,`,
|
`,foo/,`,
|
||||||
|
@ -118,7 +118,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
`Hello, World &tc!`,
|
`Hello, World &tc!`,
|
||||||
` dir="ltr"`,
|
` dir="ltr"`,
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
`Hello, World & O'Reilly\x21`,
|
`Hello, World & O'Reilly\u0021`,
|
||||||
`greeting=H%69,&addressee=(World)`,
|
`greeting=H%69,&addressee=(World)`,
|
||||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||||
`,foo/,`,
|
`,foo/,`,
|
||||||
|
@ -133,7 +133,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
`Hello, <b>World</b> &tc!`,
|
`Hello, <b>World</b> &tc!`,
|
||||||
` dir="ltr"`,
|
` dir="ltr"`,
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
`Hello, World & O'Reilly\x21`,
|
`Hello, World & O'Reilly\u0021`,
|
||||||
`greeting=H%69,&addressee=(World)`,
|
`greeting=H%69,&addressee=(World)`,
|
||||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||||
`,foo/,`,
|
`,foo/,`,
|
||||||
|
@ -149,7 +149,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
// Not escaped.
|
// Not escaped.
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
// Escape sequence not over-escaped.
|
// 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)"`,
|
||||||
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
||||||
`",foo/,"`,
|
`",foo/,"`,
|
||||||
|
@ -165,7 +165,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
// Not JS escaped but HTML escaped.
|
// Not JS escaped but HTML escaped.
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
// Escape sequence not over-escaped.
|
// 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)"`,
|
||||||
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
||||||
`",foo/,"`,
|
`",foo/,"`,
|
||||||
|
@ -174,30 +174,30 @@ func TestTypedContent(t *testing.T) {
|
||||||
{
|
{
|
||||||
`<script>alert("{{.}}")</script>`,
|
`<script>alert("{{.}}")</script>`,
|
||||||
[]string{
|
[]string{
|
||||||
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
|
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
|
||||||
`a[href =~ \x22\/\/example.com\x22]#foo`,
|
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
|
||||||
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
|
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
|
||||||
` dir=\x22ltr\x22`,
|
` dir=\u0022ltr\u0022`,
|
||||||
`c \x26\x26 alert(\x22Hello, World!\x22);`,
|
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
|
||||||
// Escape sequence not over-escaped.
|
// Escape sequence not over-escaped.
|
||||||
`Hello, World \x26 O\x27Reilly\x21`,
|
`Hello, World \u0026 O\u0027Reilly\u0021`,
|
||||||
`greeting=H%69,\x26addressee=(World)`,
|
`greeting=H%69,\u0026addressee=(World)`,
|
||||||
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
||||||
`,foo\/,`,
|
`,foo\/,`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`<script type="text/javascript">alert("{{.}}")</script>`,
|
`<script type="text/javascript">alert("{{.}}")</script>`,
|
||||||
[]string{
|
[]string{
|
||||||
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
|
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
|
||||||
`a[href =~ \x22\/\/example.com\x22]#foo`,
|
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
|
||||||
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
|
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
|
||||||
` dir=\x22ltr\x22`,
|
` dir=\u0022ltr\u0022`,
|
||||||
`c \x26\x26 alert(\x22Hello, World!\x22);`,
|
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
|
||||||
// Escape sequence not over-escaped.
|
// Escape sequence not over-escaped.
|
||||||
`Hello, World \x26 O\x27Reilly\x21`,
|
`Hello, World \u0026 O\u0027Reilly\u0021`,
|
||||||
`greeting=H%69,\x26addressee=(World)`,
|
`greeting=H%69,\u0026addressee=(World)`,
|
||||||
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
||||||
`,foo\/,`,
|
`,foo\/,`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -211,7 +211,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
// Not escaped.
|
// Not escaped.
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
// Escape sequence not over-escaped.
|
// 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)"`,
|
||||||
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
||||||
`",foo/,"`,
|
`",foo/,"`,
|
||||||
|
@ -227,7 +227,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
`Hello, <b>World</b> &tc!`,
|
`Hello, <b>World</b> &tc!`,
|
||||||
` dir="ltr"`,
|
` dir="ltr"`,
|
||||||
`c && alert("Hello, World!");`,
|
`c && alert("Hello, World!");`,
|
||||||
`Hello, World & O'Reilly\x21`,
|
`Hello, World & O'Reilly\u0021`,
|
||||||
`greeting=H%69,&addressee=(World)`,
|
`greeting=H%69,&addressee=(World)`,
|
||||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||||
`,foo/,`,
|
`,foo/,`,
|
||||||
|
@ -236,15 +236,15 @@ func TestTypedContent(t *testing.T) {
|
||||||
{
|
{
|
||||||
`<button onclick='alert("{{.}}")'>`,
|
`<button onclick='alert("{{.}}")'>`,
|
||||||
[]string{
|
[]string{
|
||||||
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
|
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
|
||||||
`a[href =~ \x22\/\/example.com\x22]#foo`,
|
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
|
||||||
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
|
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
|
||||||
` dir=\x22ltr\x22`,
|
` dir=\u0022ltr\u0022`,
|
||||||
`c \x26\x26 alert(\x22Hello, World!\x22);`,
|
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
|
||||||
// Escape sequence not over-escaped.
|
// Escape sequence not over-escaped.
|
||||||
`Hello, World \x26 O\x27Reilly\x21`,
|
`Hello, World \u0026 O\u0027Reilly\u0021`,
|
||||||
`greeting=H%69,\x26addressee=(World)`,
|
`greeting=H%69,\u0026addressee=(World)`,
|
||||||
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
||||||
`,foo\/,`,
|
`,foo\/,`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -256,7 +256,7 @@ func TestTypedContent(t *testing.T) {
|
||||||
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
|
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
|
||||||
`%20dir%3d%22ltr%22`,
|
`%20dir%3d%22ltr%22`,
|
||||||
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
|
`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.
|
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done.
|
||||||
`greeting=H%69,&addressee=%28World%29`,
|
`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`,
|
`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`,
|
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
|
||||||
`%20dir%3d%22ltr%22`,
|
`%20dir%3d%22ltr%22`,
|
||||||
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
|
`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.
|
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done.
|
||||||
`greeting=H%69,&addressee=%28World%29`,
|
`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`,
|
`greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,
|
||||||
|
|
|
@ -73,6 +73,51 @@ functions.
|
||||||
For these internal escaping functions, if an action pipeline evaluates to
|
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.
|
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
|
Errors
|
||||||
|
|
||||||
See the documentation of ErrorCode for details.
|
See the documentation of ErrorCode for details.
|
||||||
|
|
|
@ -242,7 +242,7 @@ func TestEscape(t *testing.T) {
|
||||||
{
|
{
|
||||||
"jsStr",
|
"jsStr",
|
||||||
"<button onclick='alert("{{.H}}")'>",
|
"<button onclick='alert("{{.H}}")'>",
|
||||||
`<button onclick='alert("\x3cHello\x3e")'>`,
|
`<button onclick='alert("\u003cHello\u003e")'>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"badMarshaler",
|
"badMarshaler",
|
||||||
|
@ -263,7 +263,7 @@ func TestEscape(t *testing.T) {
|
||||||
{
|
{
|
||||||
"jsRe",
|
"jsRe",
|
||||||
`<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
|
`<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
|
||||||
`<button onclick='alert(/foo\x2bbar/.test(""))'>`,
|
`<button onclick='alert(/foo\u002bbar/.test(""))'>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jsReBlank",
|
"jsReBlank",
|
||||||
|
@ -829,7 +829,7 @@ func TestEscapeSet(t *testing.T) {
|
||||||
"main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
|
"main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
|
||||||
"helper": `{{11}} of {{"<100>"}}`,
|
"helper": `{{11}} of {{"<100>"}}`,
|
||||||
},
|
},
|
||||||
`<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`,
|
`<button onclick="title='11 of \u003c100\u003e'; ...">11 of <100></button>`,
|
||||||
},
|
},
|
||||||
// A non-recursive template that ends in a different context.
|
// A non-recursive template that ends in a different context.
|
||||||
// helper starts in jsCtxRegexp and ends in jsCtxDivOp.
|
// helper starts in jsCtxRegexp and ends in jsCtxDivOp.
|
||||||
|
|
|
@ -119,9 +119,9 @@ func Example_escape() {
|
||||||
// "Fran & Freddie's Diner" <tasty@example.com>
|
// "Fran & Freddie's Diner" <tasty@example.com>
|
||||||
// "Fran & Freddie's Diner" <tasty@example.com>
|
// "Fran & Freddie's Diner" <tasty@example.com>
|
||||||
// "Fran & Freddie's Diner"32<tasty@example.com>
|
// "Fran & Freddie's Diner"32<tasty@example.com>
|
||||||
// \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
|
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
|
||||||
// \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
|
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
|
||||||
// \"Fran & Freddie\'s Diner\"32\x3Ctasty@example.com\x3E
|
// \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
|
||||||
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
|
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,6 @@ func jsValEscaper(args ...interface{}) string {
|
||||||
}
|
}
|
||||||
// TODO: detect cycles before calling Marshal which loops infinitely on
|
// TODO: detect cycles before calling Marshal which loops infinitely on
|
||||||
// cyclic data. This may be an unacceptable DoS risk.
|
// cyclic data. This may be an unacceptable DoS risk.
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
b, err := json.Marshal(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Put a space before comment so that if it is flush against
|
// 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
|
// TODO: maybe post-process output to prevent it from containing
|
||||||
// "<!--", "-->", "<![CDATA[", "]]>", or "</script"
|
// "<!--", "-->", "<![CDATA[", "]]>", or "</script"
|
||||||
// in case custom marshalers produce output containing those.
|
// in case custom marshalers produce output containing those.
|
||||||
|
// Note: Do not use \x escaping to save bytes because it is not JSON compatible and this escaper
|
||||||
// TODO: Maybe abbreviate \u00ab to \xab to produce more compact output.
|
// supports ld+json content-type.
|
||||||
if len(b) == 0 {
|
if len(b) == 0 {
|
||||||
// In, `x=y/{{.}}*z` a json.Marshaler that produces "" should
|
// In, `x=y/{{.}}*z` a json.Marshaler that produces "" should
|
||||||
// not cause the output `x=y/*z`.
|
// not cause the output `x=y/*z`.
|
||||||
|
@ -261,6 +260,8 @@ func replace(s string, replacementTable []string) string {
|
||||||
r, w = utf8.DecodeRuneInString(s[i:])
|
r, w = utf8.DecodeRuneInString(s[i:])
|
||||||
var repl string
|
var repl string
|
||||||
switch {
|
switch {
|
||||||
|
case int(r) < len(lowUnicodeReplacementTable):
|
||||||
|
repl = lowUnicodeReplacementTable[r]
|
||||||
case int(r) < len(replacementTable) && replacementTable[r] != "":
|
case int(r) < len(replacementTable) && replacementTable[r] != "":
|
||||||
repl = replacementTable[r]
|
repl = replacementTable[r]
|
||||||
case r == '\u2028':
|
case r == '\u2028':
|
||||||
|
@ -284,67 +285,80 @@ func replace(s string, replacementTable []string) string {
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsStrReplacementTable = []string{
|
var lowUnicodeReplacementTable = []string{
|
||||||
0: `\0`,
|
0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
|
||||||
|
'\a': `\u0007`,
|
||||||
|
'\b': `\u0008`,
|
||||||
'\t': `\t`,
|
'\t': `\t`,
|
||||||
'\n': `\n`,
|
'\n': `\n`,
|
||||||
'\v': `\x0b`, // "\v" == "v" on IE 6.
|
'\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: `\u0000`,
|
||||||
|
'\t': `\t`,
|
||||||
|
'\n': `\n`,
|
||||||
|
'\v': `\u000b`, // "\v" == "v" on IE 6.
|
||||||
'\f': `\f`,
|
'\f': `\f`,
|
||||||
'\r': `\r`,
|
'\r': `\r`,
|
||||||
// Encode HTML specials as hex so the output can be embedded
|
// Encode HTML specials as hex so the output can be embedded
|
||||||
// in HTML attributes without further encoding.
|
// in HTML attributes without further encoding.
|
||||||
'"': `\x22`,
|
'"': `\u0022`,
|
||||||
'&': `\x26`,
|
'&': `\u0026`,
|
||||||
'\'': `\x27`,
|
'\'': `\u0027`,
|
||||||
'+': `\x2b`,
|
'+': `\u002b`,
|
||||||
'/': `\/`,
|
'/': `\/`,
|
||||||
'<': `\x3c`,
|
'<': `\u003c`,
|
||||||
'>': `\x3e`,
|
'>': `\u003e`,
|
||||||
'\\': `\\`,
|
'\\': `\\`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsStrNormReplacementTable is like jsStrReplacementTable but does not
|
// jsStrNormReplacementTable is like jsStrReplacementTable but does not
|
||||||
// overencode existing escapes since this table has no entry for `\`.
|
// overencode existing escapes since this table has no entry for `\`.
|
||||||
var jsStrNormReplacementTable = []string{
|
var jsStrNormReplacementTable = []string{
|
||||||
0: `\0`,
|
0: `\u0000`,
|
||||||
'\t': `\t`,
|
'\t': `\t`,
|
||||||
'\n': `\n`,
|
'\n': `\n`,
|
||||||
'\v': `\x0b`, // "\v" == "v" on IE 6.
|
'\v': `\u000b`, // "\v" == "v" on IE 6.
|
||||||
'\f': `\f`,
|
'\f': `\f`,
|
||||||
'\r': `\r`,
|
'\r': `\r`,
|
||||||
// Encode HTML specials as hex so the output can be embedded
|
// Encode HTML specials as hex so the output can be embedded
|
||||||
// in HTML attributes without further encoding.
|
// in HTML attributes without further encoding.
|
||||||
'"': `\x22`,
|
'"': `\u0022`,
|
||||||
'&': `\x26`,
|
'&': `\u0026`,
|
||||||
'\'': `\x27`,
|
'\'': `\u0027`,
|
||||||
'+': `\x2b`,
|
'+': `\u002b`,
|
||||||
'/': `\/`,
|
'/': `\/`,
|
||||||
'<': `\x3c`,
|
'<': `\u003c`,
|
||||||
'>': `\x3e`,
|
'>': `\u003e`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsRegexpReplacementTable = []string{
|
var jsRegexpReplacementTable = []string{
|
||||||
0: `\0`,
|
0: `\u0000`,
|
||||||
'\t': `\t`,
|
'\t': `\t`,
|
||||||
'\n': `\n`,
|
'\n': `\n`,
|
||||||
'\v': `\x0b`, // "\v" == "v" on IE 6.
|
'\v': `\u000b`, // "\v" == "v" on IE 6.
|
||||||
'\f': `\f`,
|
'\f': `\f`,
|
||||||
'\r': `\r`,
|
'\r': `\r`,
|
||||||
// Encode HTML specials as hex so the output can be embedded
|
// Encode HTML specials as hex so the output can be embedded
|
||||||
// in HTML attributes without further encoding.
|
// in HTML attributes without further encoding.
|
||||||
'"': `\x22`,
|
'"': `\u0022`,
|
||||||
'$': `\$`,
|
'$': `\$`,
|
||||||
'&': `\x26`,
|
'&': `\u0026`,
|
||||||
'\'': `\x27`,
|
'\'': `\u0027`,
|
||||||
'(': `\(`,
|
'(': `\(`,
|
||||||
')': `\)`,
|
')': `\)`,
|
||||||
'*': `\*`,
|
'*': `\*`,
|
||||||
'+': `\x2b`,
|
'+': `\u002b`,
|
||||||
'-': `\-`,
|
'-': `\-`,
|
||||||
'.': `\.`,
|
'.': `\.`,
|
||||||
'/': `\/`,
|
'/': `\/`,
|
||||||
'<': `\x3c`,
|
'<': `\u003c`,
|
||||||
'>': `\x3e`,
|
'>': `\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/rfc7231#section-3.1.1
|
||||||
// https://tools.ietf.org/html/rfc4329#section-3
|
// https://tools.ietf.org/html/rfc4329#section-3
|
||||||
// https://www.ietf.org/rfc/rfc4627.txt
|
// https://www.ietf.org/rfc/rfc4627.txt
|
||||||
mimeType = strings.ToLower(mimeType)
|
|
||||||
// discard parameters
|
// discard parameters
|
||||||
if i := strings.Index(mimeType, ";"); i >= 0 {
|
if i := strings.Index(mimeType, ";"); i >= 0 {
|
||||||
mimeType = mimeType[:i]
|
mimeType = mimeType[:i]
|
||||||
}
|
}
|
||||||
|
mimeType = strings.ToLower(mimeType)
|
||||||
mimeType = strings.TrimSpace(mimeType)
|
mimeType = strings.TrimSpace(mimeType)
|
||||||
switch mimeType {
|
switch mimeType {
|
||||||
case
|
case
|
||||||
|
|
|
@ -139,7 +139,7 @@ func TestJSValEscaper(t *testing.T) {
|
||||||
{"foo", `"foo"`},
|
{"foo", `"foo"`},
|
||||||
// Newlines.
|
// Newlines.
|
||||||
{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
|
{"\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"`},
|
{"\t\x0b", `"\t\u000b"`},
|
||||||
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
|
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
|
||||||
{[]interface{}{}, "[]"},
|
{[]interface{}{}, "[]"},
|
||||||
|
@ -175,7 +175,7 @@ func TestJSStrEscaper(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"", ``},
|
{"", ``},
|
||||||
{"foo", `foo`},
|
{"foo", `foo`},
|
||||||
{"\u0000", `\0`},
|
{"\u0000", `\u0000`},
|
||||||
{"\t", `\t`},
|
{"\t", `\t`},
|
||||||
{"\n", `\n`},
|
{"\n", `\n`},
|
||||||
{"\r", `\r`},
|
{"\r", `\r`},
|
||||||
|
@ -185,14 +185,14 @@ func TestJSStrEscaper(t *testing.T) {
|
||||||
{"\\n", `\\n`},
|
{"\\n", `\\n`},
|
||||||
{"foo\r\nbar", `foo\r\nbar`},
|
{"foo\r\nbar", `foo\r\nbar`},
|
||||||
// Preserve attribute boundaries.
|
// Preserve attribute boundaries.
|
||||||
{`"`, `\x22`},
|
{`"`, `\u0022`},
|
||||||
{`'`, `\x27`},
|
{`'`, `\u0027`},
|
||||||
// Allow embedding in HTML without further escaping.
|
// Allow embedding in HTML without further escaping.
|
||||||
{`&`, `\x26amp;`},
|
{`&`, `\u0026amp;`},
|
||||||
// Prevent breaking out of text node and element boundaries.
|
// Prevent breaking out of text node and element boundaries.
|
||||||
{"</script>", `\x3c\/script\x3e`},
|
{"</script>", `\u003c\/script\u003e`},
|
||||||
{"<![CDATA[", `\x3c![CDATA[`},
|
{"<![CDATA[", `\u003c![CDATA[`},
|
||||||
{"]]>", `]]\x3e`},
|
{"]]>", `]]\u003e`},
|
||||||
// https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
|
// https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
|
||||||
// "The text in style, script, title, and textarea elements
|
// "The text in style, script, title, and textarea elements
|
||||||
// must not have an escaping text span start that is not
|
// 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
|
// allow regular text content to be interpreted as script
|
||||||
// allowing script execution via a combination of a JS string
|
// allowing script execution via a combination of a JS string
|
||||||
// injection followed by an HTML text injection.
|
// injection followed by an HTML text injection.
|
||||||
{"<!--", `\x3c!--`},
|
{"<!--", `\u003c!--`},
|
||||||
{"-->", `--\x3e`},
|
{"-->", `--\u003e`},
|
||||||
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
|
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
|
||||||
{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
|
{"+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
|
// Invalid UTF-8 sequence
|
||||||
{"foo\xA0bar", "foo\xA0bar"},
|
{"foo\xA0bar", "foo\xA0bar"},
|
||||||
|
@ -230,7 +230,7 @@ func TestJSRegexpEscaper(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"", `(?:)`},
|
{"", `(?:)`},
|
||||||
{"foo", `foo`},
|
{"foo", `foo`},
|
||||||
{"\u0000", `\0`},
|
{"\u0000", `\u0000`},
|
||||||
{"\t", `\t`},
|
{"\t", `\t`},
|
||||||
{"\n", `\n`},
|
{"\n", `\n`},
|
||||||
{"\r", `\r`},
|
{"\r", `\r`},
|
||||||
|
@ -240,19 +240,19 @@ func TestJSRegexpEscaper(t *testing.T) {
|
||||||
{"\\n", `\\n`},
|
{"\\n", `\\n`},
|
||||||
{"foo\r\nbar", `foo\r\nbar`},
|
{"foo\r\nbar", `foo\r\nbar`},
|
||||||
// Preserve attribute boundaries.
|
// Preserve attribute boundaries.
|
||||||
{`"`, `\x22`},
|
{`"`, `\u0022`},
|
||||||
{`'`, `\x27`},
|
{`'`, `\u0027`},
|
||||||
// Allow embedding in HTML without further escaping.
|
// Allow embedding in HTML without further escaping.
|
||||||
{`&`, `\x26amp;`},
|
{`&`, `\u0026amp;`},
|
||||||
// Prevent breaking out of text node and element boundaries.
|
// Prevent breaking out of text node and element boundaries.
|
||||||
{"</script>", `\x3c\/script\x3e`},
|
{"</script>", `\u003c\/script\u003e`},
|
||||||
{"<![CDATA[", `\x3c!\[CDATA\[`},
|
{"<![CDATA[", `\u003c!\[CDATA\[`},
|
||||||
{"]]>", `\]\]\x3e`},
|
{"]]>", `\]\]\u003e`},
|
||||||
// Escaping text spans.
|
// Escaping text spans.
|
||||||
{"<!--", `\x3c!\-\-`},
|
{"<!--", `\u003c!\-\-`},
|
||||||
{"-->", `\-\-\x3e`},
|
{"-->", `\-\-\u003e`},
|
||||||
{"*", `\*`},
|
{"*", `\*`},
|
||||||
{"+", `\x2b`},
|
{"+", `\u002b`},
|
||||||
{"?", `\?`},
|
{"?", `\?`},
|
||||||
{"[](){}", `\[\]\(\)\{\}`},
|
{"[](){}", `\[\]\(\)\{\}`},
|
||||||
{"$foo|x.y", `\$foo\|x\.y`},
|
{"$foo|x.y", `\$foo\|x\.y`},
|
||||||
|
@ -286,27 +286,27 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
|
||||||
{
|
{
|
||||||
"jsStrEscaper",
|
"jsStrEscaper",
|
||||||
jsStrEscaper,
|
jsStrEscaper,
|
||||||
"\\0\x01\x02\x03\x04\x05\x06\x07" +
|
`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
|
||||||
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
|
`\u0008\t\n\u000b\f\r\u000e\u000f` +
|
||||||
"\x10\x11\x12\x13\x14\x15\x16\x17" +
|
`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
|
||||||
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
`\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
|
||||||
` !\x22#$%\x26\x27()*\x2b,-.\/` +
|
` !\u0022#$%\u0026\u0027()*\u002b,-.\/` +
|
||||||
`0123456789:;\x3c=\x3e?` +
|
`0123456789:;\u003c=\u003e?` +
|
||||||
`@ABCDEFGHIJKLMNO` +
|
`@ABCDEFGHIJKLMNO` +
|
||||||
`PQRSTUVWXYZ[\\]^_` +
|
`PQRSTUVWXYZ[\\]^_` +
|
||||||
"`abcdefghijklmno" +
|
"`abcdefghijklmno" +
|
||||||
"pqrstuvwxyz{|}~\x7f" +
|
"pqrstuvwxyz{|}~\u007f" +
|
||||||
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
|
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jsRegexpEscaper",
|
"jsRegexpEscaper",
|
||||||
jsRegexpEscaper,
|
jsRegexpEscaper,
|
||||||
"\\0\x01\x02\x03\x04\x05\x06\x07" +
|
`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
|
||||||
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
|
`\u0008\t\n\u000b\f\r\u000e\u000f` +
|
||||||
"\x10\x11\x12\x13\x14\x15\x16\x17" +
|
`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
|
||||||
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
`\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
|
||||||
` !\x22#\$%\x26\x27\(\)\*\x2b,\-\.\/` +
|
` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` +
|
||||||
`0123456789:;\x3c=\x3e\?` +
|
`0123456789:;\u003c=\u003e\?` +
|
||||||
`@ABCDEFGHIJKLMNO` +
|
`@ABCDEFGHIJKLMNO` +
|
||||||
`PQRSTUVWXYZ\[\\\]\^_` +
|
`PQRSTUVWXYZ\[\\\]\^_` +
|
||||||
"`abcdefghijklmno" +
|
"`abcdefghijklmno" +
|
||||||
|
|
|
@ -8,6 +8,7 @@ package template_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -124,6 +125,44 @@ func TestNumbers(t *testing.T) {
|
||||||
c.mustExecute(c.root, nil, "12.34 7.5")
|
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 {
|
type testCase struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
root *Template
|
root *Template
|
||||||
|
|
272
tpl/internal/go_templates/testenv/testenv.go
Normal file
272
tpl/internal/go_templates/testenv/testenv.go
Normal file
|
@ -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
|
||||||
|
}
|
11
tpl/internal/go_templates/testenv/testenv_cgo.go
Normal file
11
tpl/internal/go_templates/testenv/testenv_cgo.go
Normal file
|
@ -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
|
||||||
|
}
|
20
tpl/internal/go_templates/testenv/testenv_notwin.go
Normal file
20
tpl/internal/go_templates/testenv/testenv_notwin.go
Normal file
|
@ -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, ""
|
||||||
|
}
|
48
tpl/internal/go_templates/testenv/testenv_windows.go
Normal file
48
tpl/internal/go_templates/testenv/testenv_windows.go
Normal file
|
@ -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, ""
|
||||||
|
}
|
|
@ -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;
|
If the value of the pipeline has length zero, nothing is output;
|
||||||
otherwise, dot is set to the successive elements of the array,
|
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
|
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
|
keys are of basic type with a defined order, the elements will be
|
||||||
elements will be visited in sorted key order.
|
visited in sorted key order.
|
||||||
|
|
||||||
{{range pipeline}} T1 {{else}} T0 {{end}}
|
{{range pipeline}} T1 {{else}} T0 {{end}}
|
||||||
The value of the pipeline must be an array, slice, map, or channel.
|
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
|
(Unlike with || in Go, however, eq is a function call and all the
|
||||||
arguments will be evaluated.)
|
arguments will be evaluated.)
|
||||||
|
|
||||||
The comparison functions work on basic types only (or named basic
|
The comparison functions work on any values whose type Go defines as
|
||||||
types, such as "type Celsius float32"). They implement the Go rules
|
comparable. For basic types such as integers, the rules are relaxed:
|
||||||
for comparison of values, except that size and exact type are
|
size and exact type are ignored, so any integer value, signed or unsigned,
|
||||||
ignored, so any integer value, signed or unsigned, may be compared
|
may be compared with any other integer value. (The arithmetic value is 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.)
|
||||||
not the bit pattern, so all negative integers are less than all
|
However, as usual, one may not compare an int with a float32 and so on.
|
||||||
unsigned integers.) However, as usual, one may not compare an int
|
|
||||||
with a float32 and so on.
|
|
||||||
|
|
||||||
Associated templates
|
Associated templates
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
package template
|
package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||||
|
@ -230,21 +229,19 @@ func (t *Template) DefinedTemplates() string {
|
||||||
if t.common == nil {
|
if t.common == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var b bytes.Buffer
|
var b strings.Builder
|
||||||
for name, tmpl := range t.tmpl {
|
for name, tmpl := range t.tmpl {
|
||||||
if tmpl.Tree == nil || tmpl.Root == nil {
|
if tmpl.Tree == nil || tmpl.Root == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if b.Len() > 0 {
|
if b.Len() == 0 {
|
||||||
|
b.WriteString("; defined templates are: ")
|
||||||
|
} else {
|
||||||
b.WriteString(", ")
|
b.WriteString(", ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "%q", name)
|
fmt.Fprintf(&b, "%q", name)
|
||||||
}
|
}
|
||||||
var s string
|
return b.String()
|
||||||
if b.Len() > 0 {
|
|
||||||
s = "; defined templates are: " + b.String()
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk functions step through the major pieces of the template structure,
|
// 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.
|
// Must be a function.
|
||||||
return s.evalFunction(dot, n, cmd, cmd.Args, final)
|
return s.evalFunction(dot, n, cmd, cmd.Args, final)
|
||||||
case *parse.PipeNode:
|
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)
|
return s.evalPipeline(dot, n)
|
||||||
case *parse.VariableNode:
|
case *parse.VariableNode:
|
||||||
return s.evalVariableNode(dot, n, cmd.Args, final)
|
return s.evalVariableNode(dot, n, cmd.Args, final)
|
||||||
|
@ -499,20 +497,29 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
|
||||||
switch {
|
switch {
|
||||||
case constant.IsComplex:
|
case constant.IsComplex:
|
||||||
return reflect.ValueOf(constant.Complex128) // incontrovertible.
|
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)
|
return reflect.ValueOf(constant.Float64)
|
||||||
|
|
||||||
case constant.IsInt:
|
case constant.IsInt:
|
||||||
n := int(constant.Int64)
|
n := int(constant.Int64)
|
||||||
if int64(n) != constant.Int64 {
|
if int64(n) != constant.Int64 {
|
||||||
s.errorf("%s overflows int", constant.Text)
|
s.errorf("%s overflows int", constant.Text)
|
||||||
}
|
}
|
||||||
return reflect.ValueOf(n)
|
return reflect.ValueOf(n)
|
||||||
|
|
||||||
case constant.IsUint:
|
case constant.IsUint:
|
||||||
s.errorf("%s overflows int", constant.Text)
|
s.errorf("%s overflows int", constant.Text)
|
||||||
}
|
}
|
||||||
return zero
|
return zero
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isRuneInt(s string) bool {
|
||||||
|
return len(s) > 0 && s[0] == '\''
|
||||||
|
}
|
||||||
|
|
||||||
func isHexInt(s string) bool {
|
func isHexInt(s string) bool {
|
||||||
return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP")
|
return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP")
|
||||||
}
|
}
|
||||||
|
|
|
@ -354,6 +354,12 @@ var execTests = []execTest{
|
||||||
{"field on interface", "{{.foo}}", "<no value>", nil, true},
|
{"field on interface", "{{.foo}}", "<no value>", nil, true},
|
||||||
{"field on parenthesized 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.
|
// Method calls.
|
||||||
{".Method0", "-{{.Method0}}-", "-M0-", tVal, true},
|
{".Method0", "-{{.Method0}}-", "-M0-", tVal, true},
|
||||||
{".Method1(1234)", "-{{.Method1 1234}}-", "-1234-", 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 MUI64S", "{{index .MUI64S 3}}", "ui643", tVal, true},
|
||||||
{"map MI8S", "{{index .MI8S 3}}", "i83", tVal, true},
|
{"map MI8S", "{{index .MI8S 3}}", "i83", tVal, true},
|
||||||
{"map MUI8S", "{{index .MUI8S 2}}", "u82", tVal, true},
|
{"map MUI8S", "{{index .MUI8S 2}}", "u82", tVal, true},
|
||||||
|
{"index of an interface field", "{{index .Empty3 0}}", "7", tVal, true},
|
||||||
|
|
||||||
// Slicing.
|
// Slicing.
|
||||||
{"slice[:]", "{{slice .SI}}", "[3 4 5]", tVal, true},
|
{"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},
|
{"string[1:2]", "{{slice .S 1 2}}", "y", tVal, true},
|
||||||
{"out of range", "{{slice .S 1 5}}", "", tVal, false},
|
{"out of range", "{{slice .S 1 5}}", "", tVal, false},
|
||||||
{"3-index slice of string", "{{slice .S 1 2 2}}", "", 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.
|
// Len.
|
||||||
{"slice", "{{len .SI}}", "3", tVal, true},
|
{"slice", "{{len .SI}}", "3", tVal, true},
|
||||||
{"map", "{{len .MSI }}", "3", tVal, true},
|
{"map", "{{len .MSI }}", "3", tVal, true},
|
||||||
{"len of int", "{{len 3}}", "", tVal, false},
|
{"len of int", "{{len 3}}", "", tVal, false},
|
||||||
{"len of nothing", "{{len .Empty0}}", "", tVal, false},
|
{"len of nothing", "{{len .Empty0}}", "", tVal, false},
|
||||||
|
{"len of an interface field", "{{len .Empty3}}", "2", tVal, true},
|
||||||
|
|
||||||
// With.
|
// With.
|
||||||
{"with true", "{{with true}}{{.}}{{end}}", "true", tVal, true},
|
{"with true", "{{with true}}{{.}}{{end}}", "true", tVal, true},
|
||||||
|
@ -665,6 +674,12 @@ var execTests = []execTest{
|
||||||
{"bug17c", "{{len .NonEmptyInterfacePtS}}", "2", tVal, true},
|
{"bug17c", "{{len .NonEmptyInterfacePtS}}", "2", tVal, true},
|
||||||
{"bug17d", "{{index .NonEmptyInterfacePtS 0}}", "a", tVal, true},
|
{"bug17d", "{{index .NonEmptyInterfacePtS 0}}", "a", tVal, true},
|
||||||
{"bug17e", "{{range .NonEmptyInterfacePtS}}-{{.}}-{{end}}", "-a--b-", 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 {
|
func zeroArgs() string {
|
||||||
|
@ -898,7 +913,9 @@ func TestJSEscaping(t *testing.T) {
|
||||||
{`Go "jump" \`, `Go \"jump\" \\`},
|
{`Go "jump" \`, `Go \"jump\" \\`},
|
||||||
{`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`},
|
{`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`},
|
||||||
{"unprintable \uFDFF", `unprintable \uFDFF`},
|
{"unprintable \uFDFF", `unprintable \uFDFF`},
|
||||||
{`<html>`, `\x3Chtml\x3E`},
|
{`<html>`, `\u003Chtml\u003E`},
|
||||||
|
{`no = in attributes`, `no \u003D in attributes`},
|
||||||
|
{`' does not become HTML entity`, `\u0026#x27; does not become HTML entity`},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
s := JSEscapeString(tc.in)
|
s := JSEscapeString(tc.in)
|
||||||
|
@ -1158,19 +1175,41 @@ var cmpTests = []cmpTest{
|
||||||
{"ge .Uthree .NegOne", "true", true},
|
{"ge .Uthree .NegOne", "true", true},
|
||||||
{"eq (index `x` 0) 'x'", "true", true}, // The example that triggered this rule.
|
{"eq (index `x` 0) 'x'", "true", true}, // The example that triggered this rule.
|
||||||
{"eq (index `x` 0) 'y'", "false", true},
|
{"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
|
// Errors
|
||||||
{"eq `xy` 1", "", false}, // Different types.
|
{"eq `xy` 1", "", false}, // Different types.
|
||||||
{"eq 2 2.0", "", false}, // Different types.
|
{"eq 2 2.0", "", false}, // Different types.
|
||||||
{"lt true true", "", false}, // Unordered types.
|
{"lt true true", "", false}, // Unordered types.
|
||||||
{"lt 1+0i 1+0i", "", 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) {
|
func TestComparison(t *testing.T) {
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
var cmpStruct = struct {
|
var cmpStruct = struct {
|
||||||
Uthree, Ufour uint
|
Uthree, Ufour uint
|
||||||
NegOne, Three int
|
NegOne, Three int
|
||||||
}{3, 4, -1, 3}
|
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 {
|
for _, test := range cmpTests {
|
||||||
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
|
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
|
||||||
tmpl, err := New("empty").Parse(text)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
@ -29,31 +30,49 @@ import (
|
||||||
// type can return interface{} or reflect.Value.
|
// type can return interface{} or reflect.Value.
|
||||||
type FuncMap map[string]interface{}
|
type FuncMap map[string]interface{}
|
||||||
|
|
||||||
var builtins = FuncMap{
|
// builtins returns the FuncMap.
|
||||||
"and": and,
|
// It is not a global variable so the linker can dead code eliminate
|
||||||
"call": call,
|
// more when this isn't called. See golang.org/issue/36021.
|
||||||
"html": HTMLEscaper,
|
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
|
||||||
"index": index,
|
func builtins() FuncMap {
|
||||||
"slice": slice,
|
return FuncMap{
|
||||||
"js": JSEscaper,
|
"and": and,
|
||||||
"len": length,
|
"call": call,
|
||||||
"not": not,
|
"html": HTMLEscaper,
|
||||||
"or": or,
|
"index": index,
|
||||||
"print": fmt.Sprint,
|
"slice": slice,
|
||||||
"printf": fmt.Sprintf,
|
"js": JSEscaper,
|
||||||
"println": fmt.Sprintln,
|
"len": length,
|
||||||
"urlquery": URLQueryEscaper,
|
"not": not,
|
||||||
|
"or": or,
|
||||||
|
"print": fmt.Sprint,
|
||||||
|
"printf": fmt.Sprintf,
|
||||||
|
"println": fmt.Sprintln,
|
||||||
|
"urlquery": URLQueryEscaper,
|
||||||
|
|
||||||
// Comparisons
|
// Comparisons
|
||||||
"eq": eq, // ==
|
"eq": eq, // ==
|
||||||
"ge": ge, // >=
|
"ge": ge, // >=
|
||||||
"gt": gt, // >
|
"gt": gt, // >
|
||||||
"le": le, // <=
|
"le": le, // <=
|
||||||
"lt": lt, // <
|
"lt": lt, // <
|
||||||
"ne": ne, // !=
|
"ne": ne, // !=
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var builtinFuncs = createValueFuncs(builtins)
|
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
|
// createValueFuncs turns a FuncMap into a map[string]reflect.Value
|
||||||
func createValueFuncs(funcMap FuncMap) 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
|
return fn, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if fn := builtinFuncs[name]; fn.IsValid() {
|
if fn := builtinFuncs()[name]; fn.IsValid() {
|
||||||
return fn, true
|
return fn, true
|
||||||
}
|
}
|
||||||
return reflect.Value{}, false
|
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
|
// 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.
|
// indexed item must be a map, slice, or array.
|
||||||
func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
|
func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
|
||||||
v := indirectInterface(item)
|
item = indirectInterface(item)
|
||||||
if !v.IsValid() {
|
if !item.IsValid() {
|
||||||
return reflect.Value{}, fmt.Errorf("index of untyped nil")
|
return reflect.Value{}, fmt.Errorf("index of untyped nil")
|
||||||
}
|
}
|
||||||
for _, i := range indexes {
|
for _, index := range indexes {
|
||||||
index := indirectInterface(i)
|
index = indirectInterface(index)
|
||||||
var isNil bool
|
var isNil bool
|
||||||
if v, isNil = indirect(v); isNil {
|
if item, isNil = indirect(item); isNil {
|
||||||
return reflect.Value{}, fmt.Errorf("index of nil pointer")
|
return reflect.Value{}, fmt.Errorf("index of nil pointer")
|
||||||
}
|
}
|
||||||
switch v.Kind() {
|
switch item.Kind() {
|
||||||
case reflect.Array, reflect.Slice, reflect.String:
|
case reflect.Array, reflect.Slice, reflect.String:
|
||||||
x, err := indexArg(index, v.Len())
|
x, err := indexArg(index, item.Len())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return reflect.Value{}, err
|
return reflect.Value{}, err
|
||||||
}
|
}
|
||||||
v = v.Index(x)
|
item = item.Index(x)
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
index, err := prepareArg(index, v.Type().Key())
|
index, err := prepareArg(index, item.Type().Key())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return reflect.Value{}, err
|
return reflect.Value{}, err
|
||||||
}
|
}
|
||||||
if x := v.MapIndex(index); x.IsValid() {
|
if x := item.MapIndex(index); x.IsValid() {
|
||||||
v = x
|
item = x
|
||||||
} else {
|
} else {
|
||||||
v = reflect.Zero(v.Type().Elem())
|
item = reflect.Zero(item.Type().Elem())
|
||||||
}
|
}
|
||||||
case reflect.Invalid:
|
case reflect.Invalid:
|
||||||
// the loop holds invariant: v.IsValid()
|
// the loop holds invariant: item.IsValid()
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
default:
|
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.
|
// 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
|
// 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.
|
// argument must be a string, slice, or array.
|
||||||
func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
|
func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
|
||||||
var (
|
item = indirectInterface(item)
|
||||||
cap int
|
if !item.IsValid() {
|
||||||
v = indirectInterface(item)
|
|
||||||
)
|
|
||||||
if !v.IsValid() {
|
|
||||||
return reflect.Value{}, fmt.Errorf("slice of untyped nil")
|
return reflect.Value{}, fmt.Errorf("slice of untyped nil")
|
||||||
}
|
}
|
||||||
if len(indexes) > 3 {
|
if len(indexes) > 3 {
|
||||||
return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
|
return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
|
||||||
}
|
}
|
||||||
switch v.Kind() {
|
var cap int
|
||||||
|
switch item.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
if len(indexes) == 3 {
|
if len(indexes) == 3 {
|
||||||
return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string")
|
return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string")
|
||||||
}
|
}
|
||||||
cap = v.Len()
|
cap = item.Len()
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
cap = v.Cap()
|
cap = item.Cap()
|
||||||
default:
|
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:].
|
// 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 {
|
for i, index := range indexes {
|
||||||
x, err := indexArg(index, cap)
|
x, err := indexArg(index, cap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -276,20 +293,16 @@ func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error)
|
||||||
// Length
|
// Length
|
||||||
|
|
||||||
// length returns the length of the item, with an error if it has no defined length.
|
// length returns the length of the item, with an error if it has no defined length.
|
||||||
func length(item interface{}) (int, error) {
|
func length(item reflect.Value) (int, error) {
|
||||||
v := reflect.ValueOf(item)
|
item, isNil := indirect(item)
|
||||||
if !v.IsValid() {
|
|
||||||
return 0, fmt.Errorf("len of untyped nil")
|
|
||||||
}
|
|
||||||
v, isNil := indirect(v)
|
|
||||||
if isNil {
|
if isNil {
|
||||||
return 0, fmt.Errorf("len of nil pointer")
|
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:
|
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
|
// Function invocation
|
||||||
|
@ -297,11 +310,11 @@ func length(item interface{}) (int, error) {
|
||||||
// call returns the result of evaluating the first argument as a function.
|
// 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.
|
// 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) {
|
func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
||||||
v := indirectInterface(fn)
|
fn = indirectInterface(fn)
|
||||||
if !v.IsValid() {
|
if !fn.IsValid() {
|
||||||
return reflect.Value{}, fmt.Errorf("call of nil")
|
return reflect.Value{}, fmt.Errorf("call of nil")
|
||||||
}
|
}
|
||||||
typ := v.Type()
|
typ := fn.Type()
|
||||||
if typ.Kind() != reflect.Func {
|
if typ.Kind() != reflect.Func {
|
||||||
return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
|
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))
|
argv := make([]reflect.Value, len(args))
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
value := indirectInterface(arg)
|
arg = indirectInterface(arg)
|
||||||
// Compute the expected type. Clumsy because of variadics.
|
// Compute the expected type. Clumsy because of variadics.
|
||||||
argType := dddType
|
argType := dddType
|
||||||
if !typ.IsVariadic() || i < numIn-1 {
|
if !typ.IsVariadic() || i < numIn-1 {
|
||||||
|
@ -330,11 +343,11 @@ func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err 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 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
|
// 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 || ...
|
// eq evaluates the comparison a == b || a == c || ...
|
||||||
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
|
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
|
||||||
v1 := indirectInterface(arg1)
|
arg1 = indirectInterface(arg1)
|
||||||
k1, err := basicKind(v1)
|
if arg1 != zero {
|
||||||
if err != nil {
|
if t1 := arg1.Type(); !t1.Comparable() {
|
||||||
return false, err
|
return false, fmt.Errorf("uncomparable type %s: %v", t1, arg1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(arg2) == 0 {
|
if len(arg2) == 0 {
|
||||||
return false, errNoComparison
|
return false, errNoComparison
|
||||||
}
|
}
|
||||||
|
k1, _ := basicKind(arg1)
|
||||||
for _, arg := range arg2 {
|
for _, arg := range arg2 {
|
||||||
v2 := indirectInterface(arg)
|
arg = indirectInterface(arg)
|
||||||
k2, err := basicKind(v2)
|
k2, _ := basicKind(arg)
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
truth := false
|
truth := false
|
||||||
if k1 != k2 {
|
if k1 != k2 {
|
||||||
// Special case: Can compare integer values regardless of type's sign.
|
// Special case: Can compare integer values regardless of type's sign.
|
||||||
switch {
|
switch {
|
||||||
case k1 == intKind && k2 == uintKind:
|
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:
|
case k1 == uintKind && k2 == intKind:
|
||||||
truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
|
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
|
||||||
default:
|
default:
|
||||||
return false, errBadComparison
|
return false, errBadComparison
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch k1 {
|
switch k1 {
|
||||||
case boolKind:
|
case boolKind:
|
||||||
truth = v1.Bool() == v2.Bool()
|
truth = arg1.Bool() == arg.Bool()
|
||||||
case complexKind:
|
case complexKind:
|
||||||
truth = v1.Complex() == v2.Complex()
|
truth = arg1.Complex() == arg.Complex()
|
||||||
case floatKind:
|
case floatKind:
|
||||||
truth = v1.Float() == v2.Float()
|
truth = arg1.Float() == arg.Float()
|
||||||
case intKind:
|
case intKind:
|
||||||
truth = v1.Int() == v2.Int()
|
truth = arg1.Int() == arg.Int()
|
||||||
case stringKind:
|
case stringKind:
|
||||||
truth = v1.String() == v2.String()
|
truth = arg1.String() == arg.String()
|
||||||
case uintKind:
|
case uintKind:
|
||||||
truth = v1.Uint() == v2.Uint()
|
truth = arg1.Uint() == arg.Uint()
|
||||||
default:
|
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 {
|
if truth {
|
||||||
|
@ -499,13 +518,13 @@ func ne(arg1, arg2 reflect.Value) (bool, error) {
|
||||||
|
|
||||||
// lt evaluates the comparison a < b.
|
// lt evaluates the comparison a < b.
|
||||||
func lt(arg1, arg2 reflect.Value) (bool, error) {
|
func lt(arg1, arg2 reflect.Value) (bool, error) {
|
||||||
v1 := indirectInterface(arg1)
|
arg1 = indirectInterface(arg1)
|
||||||
k1, err := basicKind(v1)
|
k1, err := basicKind(arg1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
v2 := indirectInterface(arg2)
|
arg2 = indirectInterface(arg2)
|
||||||
k2, err := basicKind(v2)
|
k2, err := basicKind(arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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.
|
// Special case: Can compare integer values regardless of type's sign.
|
||||||
switch {
|
switch {
|
||||||
case k1 == intKind && k2 == uintKind:
|
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:
|
case k1 == uintKind && k2 == intKind:
|
||||||
truth = v2.Int() >= 0 && v1.Uint() < uint64(v2.Int())
|
truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int())
|
||||||
default:
|
default:
|
||||||
return false, errBadComparison
|
return false, errBadComparison
|
||||||
}
|
}
|
||||||
|
@ -525,13 +544,13 @@ func lt(arg1, arg2 reflect.Value) (bool, error) {
|
||||||
case boolKind, complexKind:
|
case boolKind, complexKind:
|
||||||
return false, errBadComparisonType
|
return false, errBadComparisonType
|
||||||
case floatKind:
|
case floatKind:
|
||||||
truth = v1.Float() < v2.Float()
|
truth = arg1.Float() < arg2.Float()
|
||||||
case intKind:
|
case intKind:
|
||||||
truth = v1.Int() < v2.Int()
|
truth = arg1.Int() < arg2.Int()
|
||||||
case stringKind:
|
case stringKind:
|
||||||
truth = v1.String() < v2.String()
|
truth = arg1.String() < arg2.String()
|
||||||
case uintKind:
|
case uintKind:
|
||||||
truth = v1.Uint() < v2.Uint()
|
truth = arg1.Uint() < arg2.Uint()
|
||||||
default:
|
default:
|
||||||
panic("invalid kind")
|
panic("invalid kind")
|
||||||
}
|
}
|
||||||
|
@ -634,8 +653,10 @@ var (
|
||||||
jsBackslash = []byte(`\\`)
|
jsBackslash = []byte(`\\`)
|
||||||
jsApos = []byte(`\'`)
|
jsApos = []byte(`\'`)
|
||||||
jsQuot = []byte(`\"`)
|
jsQuot = []byte(`\"`)
|
||||||
jsLt = []byte(`\x3C`)
|
jsLt = []byte(`\u003C`)
|
||||||
jsGt = []byte(`\x3E`)
|
jsGt = []byte(`\u003E`)
|
||||||
|
jsAmp = []byte(`\u0026`)
|
||||||
|
jsEq = []byte(`\u003D`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
|
// 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)
|
w.Write(jsLt)
|
||||||
case '>':
|
case '>':
|
||||||
w.Write(jsGt)
|
w.Write(jsGt)
|
||||||
|
case '&':
|
||||||
|
w.Write(jsAmp)
|
||||||
|
case '=':
|
||||||
|
w.Write(jsEq)
|
||||||
default:
|
default:
|
||||||
w.Write(jsLowUni)
|
w.Write(jsLowUni)
|
||||||
t, b := c>>4, c&0x0f
|
t, b := c>>4, c&0x0f
|
||||||
|
@ -698,7 +723,7 @@ func JSEscapeString(s string) string {
|
||||||
|
|
||||||
func jsIsSpecial(r rune) bool {
|
func jsIsSpecial(r rune) bool {
|
||||||
switch r {
|
switch r {
|
||||||
case '\\', '\'', '"', '<', '>':
|
case '\\', '\'', '"', '<', '>', '&', '=':
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return r < ' ' || utf8.RuneSelf <= r
|
return r < ' ' || utf8.RuneSelf <= r
|
||||||
|
|
|
@ -30,7 +30,7 @@ package is auto generated.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Export it so we can populate Hugo's func map with it, which makes it faster.
|
// 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.
|
// Preparer prepares the template before execution.
|
||||||
type Preparer interface {
|
type Preparer interface {
|
||||||
|
|
|
@ -244,7 +244,7 @@ func TestAddParseTree(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Add a new parse tree.
|
// Add a new parse tree.
|
||||||
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
|
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -411,7 +411,6 @@ func lexInsideAction(l *lexer) stateFn {
|
||||||
}
|
}
|
||||||
case r <= unicode.MaxASCII && unicode.IsPrint(r):
|
case r <= unicode.MaxASCII && unicode.IsPrint(r):
|
||||||
l.emit(itemChar)
|
l.emit(itemChar)
|
||||||
return lexInsideAction
|
|
||||||
default:
|
default:
|
||||||
return l.errorf("unrecognized character in action: %#U", r)
|
return l.errorf("unrecognized character in action: %#U", r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
package parse
|
package parse
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -29,6 +28,8 @@ type Node interface {
|
||||||
// tree returns the containing *Tree.
|
// tree returns the containing *Tree.
|
||||||
// It is unexported so all implementations of Node are in this package.
|
// It is unexported so all implementations of Node are in this package.
|
||||||
tree() *Tree
|
tree() *Tree
|
||||||
|
// writeTo writes the String output to the builder.
|
||||||
|
writeTo(*strings.Builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeType identifies the type of a parse tree node.
|
// NodeType identifies the type of a parse tree node.
|
||||||
|
@ -94,11 +95,15 @@ func (l *ListNode) tree() *Tree {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ListNode) String() string {
|
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 {
|
for _, n := range l.Nodes {
|
||||||
fmt.Fprint(b, n)
|
n.writeTo(sb)
|
||||||
}
|
}
|
||||||
return b.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ListNode) CopyList() *ListNode {
|
func (l *ListNode) CopyList() *ListNode {
|
||||||
|
@ -132,6 +137,10 @@ func (t *TextNode) String() string {
|
||||||
return fmt.Sprintf(textFormat, t.Text)
|
return fmt.Sprintf(textFormat, t.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TextNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TextNode) tree() *Tree {
|
func (t *TextNode) tree() *Tree {
|
||||||
return t.tr
|
return t.tr
|
||||||
}
|
}
|
||||||
|
@ -160,23 +169,27 @@ func (p *PipeNode) append(command *CommandNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PipeNode) String() string {
|
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 {
|
if len(p.Decl) > 0 {
|
||||||
for i, v := range p.Decl {
|
for i, v := range p.Decl {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
s += ", "
|
sb.WriteString(", ")
|
||||||
}
|
}
|
||||||
s += v.String()
|
v.writeTo(sb)
|
||||||
}
|
}
|
||||||
s += " := "
|
sb.WriteString(" := ")
|
||||||
}
|
}
|
||||||
for i, c := range p.Cmds {
|
for i, c := range p.Cmds {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
s += " | "
|
sb.WriteString(" | ")
|
||||||
}
|
}
|
||||||
s += c.String()
|
c.writeTo(sb)
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PipeNode) tree() *Tree {
|
func (p *PipeNode) tree() *Tree {
|
||||||
|
@ -187,9 +200,9 @@ func (p *PipeNode) CopyPipe() *PipeNode {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
var vars []*VariableNode
|
vars := make([]*VariableNode, len(p.Decl))
|
||||||
for _, d := range p.Decl {
|
for i, d := range p.Decl {
|
||||||
vars = append(vars, d.Copy().(*VariableNode))
|
vars[i] = d.Copy().(*VariableNode)
|
||||||
}
|
}
|
||||||
n := p.tr.newPipeline(p.Pos, p.Line, vars)
|
n := p.tr.newPipeline(p.Pos, p.Line, vars)
|
||||||
n.IsAssign = p.IsAssign
|
n.IsAssign = p.IsAssign
|
||||||
|
@ -219,8 +232,15 @@ func (t *Tree) newAction(pos Pos, line int, pipe *PipeNode) *ActionNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ActionNode) String() string {
|
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 {
|
func (a *ActionNode) tree() *Tree {
|
||||||
|
@ -249,18 +269,24 @@ func (c *CommandNode) append(arg Node) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandNode) String() string {
|
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 {
|
for i, arg := range c.Args {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
s += " "
|
sb.WriteByte(' ')
|
||||||
}
|
}
|
||||||
if arg, ok := arg.(*PipeNode); ok {
|
if arg, ok := arg.(*PipeNode); ok {
|
||||||
s += "(" + arg.String() + ")"
|
sb.WriteByte('(')
|
||||||
|
arg.writeTo(sb)
|
||||||
|
sb.WriteByte(')')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s += arg.String()
|
arg.writeTo(sb)
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandNode) tree() *Tree {
|
func (c *CommandNode) tree() *Tree {
|
||||||
|
@ -311,6 +337,10 @@ func (i *IdentifierNode) String() string {
|
||||||
return i.Ident
|
return i.Ident
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *IdentifierNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(i.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (i *IdentifierNode) tree() *Tree {
|
func (i *IdentifierNode) tree() *Tree {
|
||||||
return i.tr
|
return i.tr
|
||||||
}
|
}
|
||||||
|
@ -333,14 +363,18 @@ func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VariableNode) String() string {
|
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 {
|
for i, id := range v.Ident {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
s += "."
|
sb.WriteByte('.')
|
||||||
}
|
}
|
||||||
s += id
|
sb.WriteString(id)
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VariableNode) tree() *Tree {
|
func (v *VariableNode) tree() *Tree {
|
||||||
|
@ -373,6 +407,10 @@ func (d *DotNode) String() string {
|
||||||
return "."
|
return "."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DotNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(d.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DotNode) tree() *Tree {
|
func (d *DotNode) tree() *Tree {
|
||||||
return d.tr
|
return d.tr
|
||||||
}
|
}
|
||||||
|
@ -403,6 +441,10 @@ func (n *NilNode) String() string {
|
||||||
return "nil"
|
return "nil"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NilNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(n.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NilNode) tree() *Tree {
|
func (n *NilNode) tree() *Tree {
|
||||||
return n.tr
|
return n.tr
|
||||||
}
|
}
|
||||||
|
@ -426,11 +468,16 @@ func (t *Tree) newField(pos Pos, ident string) *FieldNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FieldNode) String() string {
|
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 {
|
for _, id := range f.Ident {
|
||||||
s += "." + id
|
sb.WriteByte('.')
|
||||||
|
sb.WriteString(id)
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FieldNode) tree() *Tree {
|
func (f *FieldNode) tree() *Tree {
|
||||||
|
@ -469,14 +516,23 @@ func (c *ChainNode) Add(field string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChainNode) String() 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 {
|
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 {
|
for _, field := range c.Field {
|
||||||
s += "." + field
|
sb.WriteByte('.')
|
||||||
|
sb.WriteString(field)
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChainNode) tree() *Tree {
|
func (c *ChainNode) tree() *Tree {
|
||||||
|
@ -506,6 +562,10 @@ func (b *BoolNode) String() string {
|
||||||
return "false"
|
return "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BoolNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BoolNode) tree() *Tree {
|
func (b *BoolNode) tree() *Tree {
|
||||||
return b.tr
|
return b.tr
|
||||||
}
|
}
|
||||||
|
@ -639,6 +699,10 @@ func (n *NumberNode) String() string {
|
||||||
return n.Text
|
return n.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NumberNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(n.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NumberNode) tree() *Tree {
|
func (n *NumberNode) tree() *Tree {
|
||||||
return n.tr
|
return n.tr
|
||||||
}
|
}
|
||||||
|
@ -666,6 +730,10 @@ func (s *StringNode) String() string {
|
||||||
return s.Quoted
|
return s.Quoted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StringNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(s.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StringNode) tree() *Tree {
|
func (s *StringNode) tree() *Tree {
|
||||||
return s.tr
|
return s.tr
|
||||||
}
|
}
|
||||||
|
@ -690,6 +758,10 @@ func (e *endNode) String() string {
|
||||||
return "{{end}}"
|
return "{{end}}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *endNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(e.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (e *endNode) tree() *Tree {
|
func (e *endNode) tree() *Tree {
|
||||||
return e.tr
|
return e.tr
|
||||||
}
|
}
|
||||||
|
@ -718,6 +790,10 @@ func (e *elseNode) String() string {
|
||||||
return "{{else}}"
|
return "{{else}}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *elseNode) writeTo(sb *strings.Builder) {
|
||||||
|
sb.WriteString(e.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (e *elseNode) tree() *Tree {
|
func (e *elseNode) tree() *Tree {
|
||||||
return e.tr
|
return e.tr
|
||||||
}
|
}
|
||||||
|
@ -738,6 +814,12 @@ type BranchNode struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BranchNode) String() string {
|
func (b *BranchNode) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
b.writeTo(&sb)
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BranchNode) writeTo(sb *strings.Builder) {
|
||||||
name := ""
|
name := ""
|
||||||
switch b.NodeType {
|
switch b.NodeType {
|
||||||
case NodeIf:
|
case NodeIf:
|
||||||
|
@ -749,10 +831,17 @@ func (b *BranchNode) String() string {
|
||||||
default:
|
default:
|
||||||
panic("unknown branch type")
|
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 {
|
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 {
|
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 {
|
func (t *TemplateNode) String() string {
|
||||||
if t.Pipe == nil {
|
var sb strings.Builder
|
||||||
return fmt.Sprintf("{{template %q}}", t.Name)
|
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 {
|
func (t *TemplateNode) tree() *Tree {
|
||||||
|
|
|
@ -108,13 +108,8 @@ func (t *Tree) nextNonSpace() (token item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// peekNonSpace returns but does not consume the next non-space token.
|
// peekNonSpace returns but does not consume the next non-space token.
|
||||||
func (t *Tree) peekNonSpace() (token item) {
|
func (t *Tree) peekNonSpace() item {
|
||||||
for {
|
token := t.nextNonSpace()
|
||||||
token = t.next()
|
|
||||||
if token.typ != itemSpace {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.backup()
|
t.backup()
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
|
@ -306,7 +306,8 @@ var parseTests = []parseTest{
|
||||||
}
|
}
|
||||||
|
|
||||||
var builtins = map[string]interface{}{
|
var builtins = map[string]interface{}{
|
||||||
"printf": fmt.Sprintf,
|
"printf": fmt.Sprintf,
|
||||||
|
"contains": strings.Contains,
|
||||||
}
|
}
|
||||||
|
|
||||||
func testParse(doCopy bool, t *testing.T) {
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -110,20 +110,21 @@ func (t *Template) Clone() (*Template, error) {
|
||||||
|
|
||||||
// copy returns a shallow copy of t, with common set to the argument.
|
// copy returns a shallow copy of t, with common set to the argument.
|
||||||
func (t *Template) copy(c *common) *Template {
|
func (t *Template) copy(c *common) *Template {
|
||||||
nt := New(t.name)
|
return &Template{
|
||||||
nt.Tree = t.Tree
|
name: t.name,
|
||||||
nt.common = c
|
Tree: t.Tree,
|
||||||
nt.leftDelim = t.leftDelim
|
common: c,
|
||||||
nt.rightDelim = t.rightDelim
|
leftDelim: t.leftDelim,
|
||||||
return nt
|
rightDelim: t.rightDelim,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddParseTree adds parse tree for template with given name and associates it with t.
|
// AddParseTree associates the argument parse tree with the template t, giving
|
||||||
// If the template does not already exist, it will create a new one.
|
// it the specified name. If the template has not been defined, this tree becomes
|
||||||
// If the template does exist, it will be replaced.
|
// 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) {
|
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
|
||||||
t.init()
|
t.init()
|
||||||
// If the name is the name of this template, overwrite this template.
|
|
||||||
nt := t
|
nt := t
|
||||||
if name != t.name {
|
if name != t.name {
|
||||||
nt = t.New(name)
|
nt = t.New(name)
|
||||||
|
@ -197,7 +198,7 @@ func (t *Template) Lookup(name string) *Template {
|
||||||
func (t *Template) Parse(text string) (*Template, error) {
|
func (t *Template) Parse(text string) (*Template, error) {
|
||||||
t.init()
|
t.init()
|
||||||
t.muFuncs.RLock()
|
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()
|
t.muFuncs.RUnlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Reference in a new issue