Create lightweight forks of text/template and html/template
This commit also removes support for Ace and Amber templates. Updates #6594
This commit is contained in:
parent
4c804319f6
commit
167c01530b
82 changed files with 17792 additions and 264 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -22,4 +22,5 @@ dist
|
|||
|
||||
resources/sunset.jpg
|
||||
|
||||
vendor
|
||||
vendor
|
||||
|
||||
|
|
4
go.mod
4
go.mod
|
@ -16,7 +16,6 @@ require (
|
|||
github.com/disintegration/gift v1.2.1
|
||||
github.com/dlclark/regexp2 v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/frankban/quicktest v1.6.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
|
@ -53,8 +52,7 @@ require (
|
|||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/tdewolff/minify/v2 v2.6.1
|
||||
github.com/yosssi/ace v0.0.5
|
||||
github.com/yuin/goldmark v1.1.14
|
||||
github.com/yuin/goldmark v1.1.11
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191202084645-78f32c8dd6d5
|
||||
go.opencensus.io v0.22.0 // indirect
|
||||
gocloud.dev v0.15.0
|
||||
|
|
22
go.sum
22
go.sum
|
@ -108,8 +108,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
|||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q=
|
||||
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
|
@ -322,16 +320,10 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tdewolff/minify/v2 v2.5.2 h1:If/q1brvT+91oWiWnIMEGuFcwWtpB6AtLTxba78tvMs=
|
||||
github.com/tdewolff/minify/v2 v2.5.2/go.mod h1:Q6mWHrmspbdRX0ZuUUoKIT8bDjVVXpIJ73ux7p7HZGg=
|
||||
github.com/tdewolff/minify/v2 v2.6.1 h1:UJLhbs2Q/iDrqA79EEyKE48uYHeAMPVdiUzdtKsatJ8=
|
||||
github.com/tdewolff/minify/v2 v2.6.1/go.mod h1:l9hbQnH096st77OkscoRUvKdd23oUM6pDZpYx381sPo=
|
||||
github.com/tdewolff/parse/v2 v2.3.9 h1:d8/K6XOLy5JVpLTG9Kx+SxA72rlm5OowFmVSVgtOlmM=
|
||||
github.com/tdewolff/parse/v2 v2.3.9/go.mod h1:HansaqmN4I/U7L6/tUp0NcwT2tFO0F4EAWYGSDzkYNk=
|
||||
github.com/tdewolff/parse/v2 v2.3.14 h1:Tzam5YoUXx7gybFEfR/zcuR74PXADnrfUqYUXL+K5oA=
|
||||
github.com/tdewolff/parse/v2 v2.3.14/go.mod h1:+V2lSZ93xpH2Csfs/vtNY1Fjr8kcFMsZKjyLoSkZbM0=
|
||||
github.com/tdewolff/test v1.0.0 h1:jOwzqCXr5ePXEPGJaq2ivoR6HOCi+D5TPfpoyg8yvmU=
|
||||
github.com/tdewolff/test v1.0.0/go.mod h1:DiQUlutnqlEvdvhSn2LPGy4TFwRauAaYDsL+683RNX4=
|
||||
github.com/tdewolff/test v1.0.4 h1:ih38SXuQJ32Hng5EtSW32xqEsVeMnPp6nNNRPhBBDE8=
|
||||
github.com/tdewolff/test v1.0.4/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
|
@ -349,24 +341,10 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhe
|
|||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
|
||||
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
|
||||
github.com/yuin/goldmark v1.1.5 h1:JJy3EDke+PMI2WcFIU6SdaeiP6FgRGK5NKAiPZHiOoE=
|
||||
github.com/yuin/goldmark v1.1.5/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.7 h1:XiwWADvxJeIM1JbXqthrEhDc19hTMui+o+QaY1hGXlk=
|
||||
github.com/yuin/goldmark v1.1.7/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.8 h1:d0m8Ac9JaetYjPZLC4P4W32ac7I0lpJpQbvxZtFqBoM=
|
||||
github.com/yuin/goldmark v1.1.8/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.10 h1:bg3TC1aj4DbjGdhvjSSffGfAgVUdBEIpccuCozwOYWo=
|
||||
github.com/yuin/goldmark v1.1.10/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.11 h1:OO08ilczi3F4swaYWPB99s08WRxP9DdLBemiLFQ6vCo=
|
||||
github.com/yuin/goldmark v1.1.11/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.14 h1:9/OvYI+gdtQ5EAZY0y4kuVnuKjlE03BRqTw/njWYRNo=
|
||||
github.com/yuin/goldmark v1.1.14/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191124122839-ede94e40cc3a h1:L7FTUnbc0WEBqGWgjbx4sPNAOX1/q5W/3KCD6g8XkKo=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191124122839-ede94e40cc3a/go.mod h1:1gshkGdH4gcrIH5MGSScGH42rOOCO+4Ks6acjAkA9C0=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191126180129-d7a4bf4d7ea4 h1:vI4Jv29V1cMPqetuLPMW1CMB9xNgxsHVBo8Mid6bwH8=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191126180129-d7a4bf4d7ea4/go.mod h1:4QGn5rJFOASBa2uK4Q2h3BRTyJqRfsAucPFIipSTcaM=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191202084645-78f32c8dd6d5 h1:QbH7ca1qtgZHrzvcVAEoiJIwBqrXxMOfHYfwZIniIK0=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20191202084645-78f32c8dd6d5/go.mod h1:4QGn5rJFOASBa2uK4Q2h3BRTyJqRfsAucPFIipSTcaM=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
|
|
|
@ -16,7 +16,6 @@ package hugolib
|
|||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
@ -234,6 +233,7 @@ Page2: {{ $page2.Params.ColoR }}
|
|||
)
|
||||
}
|
||||
|
||||
// TODO1
|
||||
func TestCaseInsensitiveConfigurationForAllTemplateEngines(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -241,23 +241,13 @@ func TestCaseInsensitiveConfigurationForAllTemplateEngines(t *testing.T) {
|
|||
return s
|
||||
}
|
||||
|
||||
amberFixer := func(s string) string {
|
||||
fixed := strings.Replace(s, "{{ .Site.Params", "{{ Site.Params", -1)
|
||||
fixed = strings.Replace(fixed, "{{ .Params", "{{ Params", -1)
|
||||
fixed = strings.Replace(fixed, ".Content", "Content", -1)
|
||||
fixed = strings.Replace(fixed, "{{", "#{", -1)
|
||||
fixed = strings.Replace(fixed, "}}", "}", -1)
|
||||
|
||||
return fixed
|
||||
}
|
||||
|
||||
for _, config := range []struct {
|
||||
suffix string
|
||||
templateFixer func(s string) string
|
||||
}{
|
||||
{"amber", amberFixer},
|
||||
//{"amber", amberFixer},
|
||||
{"html", noOp},
|
||||
{"ace", noOp},
|
||||
//{"ace", noOp},
|
||||
} {
|
||||
doTestCaseInsensitiveConfigurationForTemplateEngine(t, config.suffix, config.templateFixer)
|
||||
|
||||
|
|
|
@ -18,34 +18,22 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
)
|
||||
|
||||
// TODO1
|
||||
func TestAllTemplateEngines(t *testing.T) {
|
||||
noOp := func(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
amberFixer := func(s string) string {
|
||||
fixed := strings.Replace(s, "{{ .Title", "{{ Title", -1)
|
||||
fixed = strings.Replace(fixed, ".Content", "Content", -1)
|
||||
fixed = strings.Replace(fixed, ".IsNamedParams", "IsNamedParams", -1)
|
||||
fixed = strings.Replace(fixed, "{{", "#{", -1)
|
||||
fixed = strings.Replace(fixed, "}}", "}", -1)
|
||||
fixed = strings.Replace(fixed, `title "hello world"`, `title("hello world")`, -1)
|
||||
|
||||
return fixed
|
||||
}
|
||||
|
||||
for _, config := range []struct {
|
||||
suffix string
|
||||
templateFixer func(s string) string
|
||||
}{
|
||||
{"amber", amberFixer},
|
||||
//{"amber", amberFixer},
|
||||
{"html", noOp},
|
||||
{"ace", noOp},
|
||||
//{"ace", noOp},
|
||||
} {
|
||||
config := config
|
||||
t.Run(config.suffix,
|
||||
|
|
|
@ -320,7 +320,7 @@ func runCmd(env map[string]string, cmd string, args ...string) error {
|
|||
}
|
||||
|
||||
func isGoLatest() bool {
|
||||
return strings.Contains(runtime.Version(), "1.12")
|
||||
return strings.Contains(runtime.Version(), "1.13")
|
||||
}
|
||||
|
||||
func isCI() bool {
|
||||
|
|
|
@ -26,8 +26,7 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
aceTemplateInnerMarkers = [][]byte{[]byte("= content")}
|
||||
goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define"), []byte("{{- define"), []byte("{{-define")}
|
||||
goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define"), []byte("{{- define"), []byte("{{-define")}
|
||||
)
|
||||
|
||||
// TemplateNames represents a template naming scheme.
|
||||
|
@ -110,8 +109,8 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) {
|
|||
id.Name = "_text/" + id.Name
|
||||
}
|
||||
|
||||
// Ace and Go templates may have both a base and inner template.
|
||||
if ext == "amber" || isShorthCodeOrPartial(name) {
|
||||
// Go templates may have both a base and inner template.
|
||||
if isShorthCodeOrPartial(name) {
|
||||
// No base template support
|
||||
return id, nil
|
||||
}
|
||||
|
@ -128,10 +127,6 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) {
|
|||
baseFilename = fmt.Sprintf("%s.%s", baseFileBase, ext)
|
||||
}
|
||||
|
||||
if ext == "ace" {
|
||||
innerMarkers = aceTemplateInnerMarkers
|
||||
}
|
||||
|
||||
// This may be a view that shouldn't have base template
|
||||
// Have to look inside it to make sure
|
||||
needsBase, err := d.ContainsAny(d.RelPath, innerMarkers)
|
||||
|
@ -152,7 +147,7 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) {
|
|||
pathsToCheck := createPathsToCheck(pathDir, baseFilename, currBaseFilename)
|
||||
|
||||
// We may have language code and/or "terms" in the template name. We want the most specific,
|
||||
// but need to fall back to the baseof.html or baseof.ace if needed.
|
||||
// but need to fall back to the baseof.html if needed.
|
||||
// E.g. list-baseof.en.html and list-baseof.terms.en.html
|
||||
// See #3893, #3856.
|
||||
baseBaseFilename, currBaseBaseFilename := helpers.Filename(baseFilename), helpers.Filename(currBaseFilename)
|
||||
|
|
1
scripts/fork_go_templates/.gitignore
vendored
Normal file
1
scripts/fork_go_templates/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
fork_go_templates
|
207
scripts/fork_go_templates/main.go
Normal file
207
scripts/fork_go_templates/main.go
Normal file
|
@ -0,0 +1,207 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO(bep) git checkout tag
|
||||
// The current is built with Go version 9341fe073e6f7742c9d61982084874560dac2014 / go1.13.5
|
||||
fmt.Println("Forking ...")
|
||||
defer fmt.Println("Done ...")
|
||||
|
||||
cleanFork()
|
||||
|
||||
htmlRoot := filepath.Join(forkRoot, "htmltemplate")
|
||||
|
||||
for _, pkg := range goPackages {
|
||||
copyGoPackage(pkg.dstPkg, pkg.srcPkg)
|
||||
}
|
||||
|
||||
for _, pkg := range goPackages {
|
||||
doWithGoFiles(pkg.dstPkg, pkg.rewriter, pkg.replacer)
|
||||
}
|
||||
|
||||
goimports(htmlRoot)
|
||||
gofmt(forkRoot)
|
||||
|
||||
}
|
||||
|
||||
const (
|
||||
// TODO(bep)
|
||||
goSource = "/Users/bep/dev/go/dump/go/src"
|
||||
forkRoot = "../../tpl/internal/go_templates"
|
||||
)
|
||||
|
||||
type goPackage struct {
|
||||
srcPkg string
|
||||
dstPkg string
|
||||
replacer func(name, content string) string
|
||||
rewriter func(name string)
|
||||
}
|
||||
|
||||
var (
|
||||
textTemplateReplacers = strings.NewReplacer(
|
||||
`"text/template/`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/`,
|
||||
`"internal/fmtsort"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`,
|
||||
// Rename types and function that we want to overload.
|
||||
"type state struct", "type stateOld struct",
|
||||
)
|
||||
|
||||
htmlTemplateReplacers = strings.NewReplacer(
|
||||
`. "html/template"`, `. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"`,
|
||||
`"html/template"`, `template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"`,
|
||||
"\"text/template\"\n", "template \"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate\"\n",
|
||||
`"html/template"`, `htmltemplate "html/template"`,
|
||||
`"fmt"`, `htmltemplate "html/template"`,
|
||||
)
|
||||
)
|
||||
|
||||
func commonReplace(name, content string) string {
|
||||
if strings.HasSuffix(name, "_test.go") {
|
||||
content = strings.Replace(content, "package template\n", `// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
`, 1)
|
||||
content = strings.Replace(content, "package template_test\n", `// +build go1.13
|
||||
|
||||
package template_test
|
||||
`, 1)
|
||||
|
||||
content = strings.Replace(content, "package parse\n", `// +build go1.13
|
||||
|
||||
package parse
|
||||
`, 1)
|
||||
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
}
|
||||
|
||||
var goPackages = []goPackage{
|
||||
goPackage{srcPkg: "text/template", dstPkg: "texttemplate",
|
||||
replacer: func(name, content string) string { return textTemplateReplacers.Replace(commonReplace(name, content)) }},
|
||||
goPackage{srcPkg: "html/template", dstPkg: "htmltemplate", replacer: func(name, content string) string {
|
||||
if strings.HasSuffix(name, "content.go") {
|
||||
// Remove template.HTML types. We need to use the Go types.
|
||||
content = removeAll(`(?s)// Strings of content.*?\)\n`, content)
|
||||
}
|
||||
|
||||
content = commonReplace(name, content)
|
||||
|
||||
return htmlTemplateReplacers.Replace(content)
|
||||
},
|
||||
rewriter: func(name string) {
|
||||
for _, s := range []string{"CSS", "HTML", "HTMLAttr", "JS", "JSStr", "URL", "Srcset"} {
|
||||
rewrite(name, fmt.Sprintf("%s -> htmltemplate.%s", s, s))
|
||||
}
|
||||
rewrite(name, `"text/template/parse" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"`)
|
||||
}},
|
||||
goPackage{srcPkg: "internal/fmtsort", dstPkg: "fmtsort", rewriter: func(name string) {
|
||||
rewrite(name, `"internal/fmtsort" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`)
|
||||
}},
|
||||
}
|
||||
|
||||
var fs = afero.NewOsFs()
|
||||
|
||||
// Removes all non-Hugo files in the go_templates folder.
|
||||
func cleanFork() {
|
||||
must(filepath.Walk(filepath.Join(forkRoot), func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && len(path) > 10 && !strings.Contains(path, "hugo") {
|
||||
must(fs.Remove(path))
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func must(err error, what ...string) {
|
||||
if err != nil {
|
||||
log.Fatal(what, " ERROR: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyGoPackage(dst, src string) {
|
||||
from := filepath.Join(goSource, src)
|
||||
to := filepath.Join(forkRoot, dst)
|
||||
fmt.Println("Copy", from, "to", to)
|
||||
must(hugio.CopyDir(fs, from, to, func(s string) bool { return true }))
|
||||
}
|
||||
|
||||
func doWithGoFiles(dir string,
|
||||
rewrite func(name string),
|
||||
transform func(name, in string) string) {
|
||||
if rewrite == nil && transform == nil {
|
||||
return
|
||||
}
|
||||
must(filepath.Walk(filepath.Join(forkRoot, dir), func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, ".go") || strings.Contains(path, "hugo_") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Handle", path)
|
||||
|
||||
if rewrite != nil {
|
||||
rewrite(path)
|
||||
}
|
||||
|
||||
if transform == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
must(err)
|
||||
f, err := os.Create(path)
|
||||
must(err)
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(transform(path, string(data)))
|
||||
must(err)
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func removeAll(expression, content string) string {
|
||||
re := regexp.MustCompile(expression)
|
||||
return re.ReplaceAllString(content, "")
|
||||
|
||||
}
|
||||
|
||||
func rewrite(filename, rule string) {
|
||||
cmf := exec.Command("gofmt", "-w", "-r", rule, filename)
|
||||
out, err := cmf.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatal("gofmt failed:", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func goimports(dir string) {
|
||||
cmf := exec.Command("goimports", "-w", dir)
|
||||
out, err := cmf.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatal("goimports failed:", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func gofmt(dir string) {
|
||||
cmf := exec.Command("gofmt", "-w", dir)
|
||||
out, err := cmf.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatal("gofmt failed:", string(out))
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ package cast
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
|
|
@ -18,6 +18,7 @@ package collections
|
|||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
|
11
tpl/internal/go_templates/fmtsort/export_test.go
Normal file
11
tpl/internal/go_templates/fmtsort/export_test.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2018 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 fmtsort
|
||||
|
||||
import "reflect"
|
||||
|
||||
func Compare(a, b reflect.Value) int {
|
||||
return compare(a, b)
|
||||
}
|
216
tpl/internal/go_templates/fmtsort/sort.go
Normal file
216
tpl/internal/go_templates/fmtsort/sort.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2018 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 fmtsort provides a general stable ordering mechanism
|
||||
// for maps, on behalf of the fmt and text/template packages.
|
||||
// It is not guaranteed to be efficient and works only for types
|
||||
// that are valid map keys.
|
||||
package fmtsort
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Note: Throughout this package we avoid calling reflect.Value.Interface as
|
||||
// it is not always legal to do so and it's easier to avoid the issue than to face it.
|
||||
|
||||
// SortedMap represents a map's keys and values. The keys and values are
|
||||
// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
|
||||
type SortedMap struct {
|
||||
Key []reflect.Value
|
||||
Value []reflect.Value
|
||||
}
|
||||
|
||||
func (o *SortedMap) Len() int { return len(o.Key) }
|
||||
func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
|
||||
func (o *SortedMap) Swap(i, j int) {
|
||||
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
|
||||
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
|
||||
}
|
||||
|
||||
// Sort accepts a map and returns a SortedMap that has the same keys and
|
||||
// values but in a stable sorted order according to the keys, modulo issues
|
||||
// raised by unorderable key values such as NaNs.
|
||||
//
|
||||
// The ordering rules are more general than with Go's < operator:
|
||||
//
|
||||
// - when applicable, nil compares low
|
||||
// - ints, floats, and strings order by <
|
||||
// - NaN compares less than non-NaN floats
|
||||
// - bool compares false before true
|
||||
// - complex compares real, then imag
|
||||
// - pointers compare by machine address
|
||||
// - channel values compare by machine address
|
||||
// - structs compare each field in turn
|
||||
// - arrays compare each element in turn.
|
||||
// Otherwise identical arrays compare by length.
|
||||
// - interface values compare first by reflect.Type describing the concrete type
|
||||
// and then by concrete value as described in the previous rules.
|
||||
//
|
||||
func Sort(mapValue reflect.Value) *SortedMap {
|
||||
if mapValue.Type().Kind() != reflect.Map {
|
||||
return nil
|
||||
}
|
||||
key := make([]reflect.Value, mapValue.Len())
|
||||
value := make([]reflect.Value, len(key))
|
||||
iter := mapValue.MapRange()
|
||||
for i := 0; iter.Next(); i++ {
|
||||
key[i] = iter.Key()
|
||||
value[i] = iter.Value()
|
||||
}
|
||||
sorted := &SortedMap{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
sort.Stable(sorted)
|
||||
return sorted
|
||||
}
|
||||
|
||||
// compare compares two values of the same type. It returns -1, 0, 1
|
||||
// according to whether a > b (1), a == b (0), or a < b (-1).
|
||||
// If the types differ, it returns -1.
|
||||
// See the comment on Sort for the comparison rules.
|
||||
func compare(aVal, bVal reflect.Value) int {
|
||||
aType, bType := aVal.Type(), bVal.Type()
|
||||
if aType != bType {
|
||||
return -1 // No good answer possible, but don't return 0: they're not equal.
|
||||
}
|
||||
switch aVal.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
a, b := aVal.Int(), bVal.Int()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
a, b := aVal.Uint(), bVal.Uint()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.String:
|
||||
a, b := aVal.String(), bVal.String()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatCompare(aVal.Float(), bVal.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
a, b := aVal.Complex(), bVal.Complex()
|
||||
if c := floatCompare(real(a), real(b)); c != 0 {
|
||||
return c
|
||||
}
|
||||
return floatCompare(imag(a), imag(b))
|
||||
case reflect.Bool:
|
||||
a, b := aVal.Bool(), bVal.Bool()
|
||||
switch {
|
||||
case a == b:
|
||||
return 0
|
||||
case a:
|
||||
return 1
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
case reflect.Ptr:
|
||||
a, b := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Chan:
|
||||
if c, ok := nilCompare(aVal, bVal); ok {
|
||||
return c
|
||||
}
|
||||
ap, bp := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case ap < bp:
|
||||
return -1
|
||||
case ap > bp:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < aVal.NumField(); i++ {
|
||||
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return 0
|
||||
case reflect.Array:
|
||||
for i := 0; i < aVal.Len(); i++ {
|
||||
if c := compare(aVal.Index(i), bVal.Index(i)); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return 0
|
||||
case reflect.Interface:
|
||||
if c, ok := nilCompare(aVal, bVal); ok {
|
||||
return c
|
||||
}
|
||||
c := compare(reflect.ValueOf(aVal.Elem().Type()), reflect.ValueOf(bVal.Elem().Type()))
|
||||
if c != 0 {
|
||||
return c
|
||||
}
|
||||
return compare(aVal.Elem(), bVal.Elem())
|
||||
default:
|
||||
// Certain types cannot appear as keys (maps, funcs, slices), but be explicit.
|
||||
panic("bad type in compare: " + aType.String())
|
||||
}
|
||||
}
|
||||
|
||||
// nilCompare checks whether either value is nil. If not, the boolean is false.
|
||||
// If either value is nil, the boolean is true and the integer is the comparison
|
||||
// value. The comparison is defined to be 0 if both are nil, otherwise the one
|
||||
// nil value compares low. Both arguments must represent a chan, func,
|
||||
// interface, map, pointer, or slice.
|
||||
func nilCompare(aVal, bVal reflect.Value) (int, bool) {
|
||||
if aVal.IsNil() {
|
||||
if bVal.IsNil() {
|
||||
return 0, true
|
||||
}
|
||||
return -1, true
|
||||
}
|
||||
if bVal.IsNil() {
|
||||
return 1, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// floatCompare compares two floating-point values. NaNs compare low.
|
||||
func floatCompare(a, b float64) int {
|
||||
switch {
|
||||
case isNaN(a):
|
||||
return -1 // No good answer if b is a NaN so don't bother checking.
|
||||
case isNaN(b):
|
||||
return 1
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func isNaN(a float64) bool {
|
||||
return a != a
|
||||
}
|
246
tpl/internal/go_templates/fmtsort/sort_test.go
Normal file
246
tpl/internal/go_templates/fmtsort/sort_test.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
// Copyright 2018 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 fmtsort_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var compareTests = [][]reflect.Value{
|
||||
ct(reflect.TypeOf(int(0)), -1, 0, 1),
|
||||
ct(reflect.TypeOf(int8(0)), -1, 0, 1),
|
||||
ct(reflect.TypeOf(int16(0)), -1, 0, 1),
|
||||
ct(reflect.TypeOf(int32(0)), -1, 0, 1),
|
||||
ct(reflect.TypeOf(int64(0)), -1, 0, 1),
|
||||
ct(reflect.TypeOf(uint(0)), 0, 1, 5),
|
||||
ct(reflect.TypeOf(uint8(0)), 0, 1, 5),
|
||||
ct(reflect.TypeOf(uint16(0)), 0, 1, 5),
|
||||
ct(reflect.TypeOf(uint32(0)), 0, 1, 5),
|
||||
ct(reflect.TypeOf(uint64(0)), 0, 1, 5),
|
||||
ct(reflect.TypeOf(uintptr(0)), 0, 1, 5),
|
||||
ct(reflect.TypeOf(string("")), "", "a", "ab"),
|
||||
ct(reflect.TypeOf(float32(0)), math.NaN(), math.Inf(-1), -1e10, 0, 1e10, math.Inf(1)),
|
||||
ct(reflect.TypeOf(float64(0)), math.NaN(), math.Inf(-1), -1e10, 0, 1e10, math.Inf(1)),
|
||||
ct(reflect.TypeOf(complex64(0+1i)), -1-1i, -1+0i, -1+1i, 0-1i, 0+0i, 0+1i, 1-1i, 1+0i, 1+1i),
|
||||
ct(reflect.TypeOf(complex128(0+1i)), -1-1i, -1+0i, -1+1i, 0-1i, 0+0i, 0+1i, 1-1i, 1+0i, 1+1i),
|
||||
ct(reflect.TypeOf(false), false, true),
|
||||
ct(reflect.TypeOf(&ints[0]), &ints[0], &ints[1], &ints[2]),
|
||||
ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]),
|
||||
ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}),
|
||||
ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}),
|
||||
ct(reflect.TypeOf(interface{}(interface{}(0))), iFace, 1, 2, 3),
|
||||
}
|
||||
|
||||
var iFace interface{}
|
||||
|
||||
func ct(typ reflect.Type, args ...interface{}) []reflect.Value {
|
||||
value := make([]reflect.Value, len(args))
|
||||
for i, v := range args {
|
||||
x := reflect.ValueOf(v)
|
||||
if !x.IsValid() { // Make it a typed nil.
|
||||
x = reflect.Zero(typ)
|
||||
} else {
|
||||
x = x.Convert(typ)
|
||||
}
|
||||
value[i] = x
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, test := range compareTests {
|
||||
for i, v0 := range test {
|
||||
for j, v1 := range test {
|
||||
c := fmtsort.Compare(v0, v1)
|
||||
var expect int
|
||||
switch {
|
||||
case i == j:
|
||||
expect = 0
|
||||
// NaNs are tricky.
|
||||
if typ := v0.Type(); (typ.Kind() == reflect.Float32 || typ.Kind() == reflect.Float64) && math.IsNaN(v0.Float()) {
|
||||
expect = -1
|
||||
}
|
||||
case i < j:
|
||||
expect = -1
|
||||
case i > j:
|
||||
expect = 1
|
||||
}
|
||||
if c != expect {
|
||||
t.Errorf("%s: compare(%v,%v)=%d; expect %d", v0.Type(), v0, v1, c, expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type sortTest struct {
|
||||
data interface{} // Always a map.
|
||||
print string // Printed result using our custom printer.
|
||||
}
|
||||
|
||||
var sortTests = []sortTest{
|
||||
{
|
||||
map[int]string{7: "bar", -3: "foo"},
|
||||
"-3:foo 7:bar",
|
||||
},
|
||||
{
|
||||
map[uint8]string{7: "bar", 3: "foo"},
|
||||
"3:foo 7:bar",
|
||||
},
|
||||
{
|
||||
map[string]string{"7": "bar", "3": "foo"},
|
||||
"3:foo 7:bar",
|
||||
},
|
||||
{
|
||||
map[float64]string{7: "bar", -3: "foo", math.NaN(): "nan", math.Inf(0): "inf"},
|
||||
"NaN:nan -3:foo 7:bar +Inf:inf",
|
||||
},
|
||||
{
|
||||
map[complex128]string{7 + 2i: "bar2", 7 + 1i: "bar", -3: "foo", complex(math.NaN(), 0i): "nan", complex(math.Inf(0), 0i): "inf"},
|
||||
"(NaN+0i):nan (-3+0i):foo (7+1i):bar (7+2i):bar2 (+Inf+0i):inf",
|
||||
},
|
||||
{
|
||||
map[bool]string{true: "true", false: "false"},
|
||||
"false:false true:true",
|
||||
},
|
||||
{
|
||||
chanMap(),
|
||||
"CHAN0:0 CHAN1:1 CHAN2:2",
|
||||
},
|
||||
{
|
||||
pointerMap(),
|
||||
"PTR0:0 PTR1:1 PTR2:2",
|
||||
},
|
||||
{
|
||||
map[toy]string{toy{7, 2}: "72", toy{7, 1}: "71", toy{3, 4}: "34"},
|
||||
"{3 4}:34 {7 1}:71 {7 2}:72",
|
||||
},
|
||||
{
|
||||
map[[2]int]string{{7, 2}: "72", {7, 1}: "71", {3, 4}: "34"},
|
||||
"[3 4]:34 [7 1]:71 [7 2]:72",
|
||||
},
|
||||
}
|
||||
|
||||
func sprint(data interface{}) string {
|
||||
om := fmtsort.Sort(reflect.ValueOf(data))
|
||||
if om == nil {
|
||||
return "nil"
|
||||
}
|
||||
b := new(strings.Builder)
|
||||
for i, key := range om.Key {
|
||||
if i > 0 {
|
||||
b.WriteRune(' ')
|
||||
}
|
||||
b.WriteString(sprintKey(key))
|
||||
b.WriteRune(':')
|
||||
b.WriteString(fmt.Sprint(om.Value[i]))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// sprintKey formats a reflect.Value but gives reproducible values for some
|
||||
// problematic types such as pointers. Note that it only does special handling
|
||||
// for the troublesome types used in the test cases; it is not a general
|
||||
// printer.
|
||||
func sprintKey(key reflect.Value) string {
|
||||
switch str := key.Type().String(); str {
|
||||
case "*int":
|
||||
ptr := key.Interface().(*int)
|
||||
for i := range ints {
|
||||
if ptr == &ints[i] {
|
||||
return fmt.Sprintf("PTR%d", i)
|
||||
}
|
||||
}
|
||||
return "PTR???"
|
||||
case "chan int":
|
||||
c := key.Interface().(chan int)
|
||||
for i := range chans {
|
||||
if c == chans[i] {
|
||||
return fmt.Sprintf("CHAN%d", i)
|
||||
}
|
||||
}
|
||||
return "CHAN???"
|
||||
default:
|
||||
return fmt.Sprint(key)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ints [3]int
|
||||
chans = [3]chan int{make(chan int), make(chan int), make(chan int)}
|
||||
)
|
||||
|
||||
func pointerMap() map[*int]string {
|
||||
m := make(map[*int]string)
|
||||
for i := 2; i >= 0; i-- {
|
||||
m[&ints[i]] = fmt.Sprint(i)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func chanMap() map[chan int]string {
|
||||
m := make(map[chan int]string)
|
||||
for i := 2; i >= 0; i-- {
|
||||
m[chans[i]] = fmt.Sprint(i)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type toy struct {
|
||||
A int // Exported.
|
||||
b int // Unexported.
|
||||
}
|
||||
|
||||
func TestOrder(t *testing.T) {
|
||||
for _, test := range sortTests {
|
||||
got := sprint(test.data)
|
||||
if got != test.print {
|
||||
t.Errorf("%s: got %q, want %q", reflect.TypeOf(test.data), got, test.print)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterface(t *testing.T) {
|
||||
// A map containing multiple concrete types should be sorted by type,
|
||||
// then value. However, the relative ordering of types is unspecified,
|
||||
// so test this by checking the presence of sorted subgroups.
|
||||
m := map[interface{}]string{
|
||||
[2]int{1, 0}: "",
|
||||
[2]int{0, 1}: "",
|
||||
true: "",
|
||||
false: "",
|
||||
3.1: "",
|
||||
2.1: "",
|
||||
1.1: "",
|
||||
math.NaN(): "",
|
||||
3: "",
|
||||
2: "",
|
||||
1: "",
|
||||
"c": "",
|
||||
"b": "",
|
||||
"a": "",
|
||||
struct{ x, y int }{1, 0}: "",
|
||||
struct{ x, y int }{0, 1}: "",
|
||||
}
|
||||
got := sprint(m)
|
||||
typeGroups := []string{
|
||||
"NaN: 1.1: 2.1: 3.1:", // float64
|
||||
"false: true:", // bool
|
||||
"1: 2: 3:", // int
|
||||
"a: b: c:", // string
|
||||
"[0 1]: [1 0]:", // [2]int
|
||||
"{0 1}: {1 0}:", // struct{ x int; y int }
|
||||
}
|
||||
for _, g := range typeGroups {
|
||||
if !strings.Contains(got, g) {
|
||||
t.Errorf("sorted map should contain %q", g)
|
||||
}
|
||||
}
|
||||
}
|
175
tpl/internal/go_templates/htmltemplate/attr.go
Normal file
175
tpl/internal/go_templates/htmltemplate/attr.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// attrTypeMap[n] describes the value of the given attribute.
|
||||
// If an attribute affects (or can mask) the encoding or interpretation of
|
||||
// other content, or affects the contents, idempotency, or credentials of a
|
||||
// network message, then the value in this map is contentTypeUnsafe.
|
||||
// This map is derived from HTML5, specifically
|
||||
// https://www.w3.org/TR/html5/Overview.html#attributes-1
|
||||
// as well as "%URI"-typed attributes from
|
||||
// https://www.w3.org/TR/html4/index/attributes.html
|
||||
var attrTypeMap = map[string]contentType{
|
||||
"accept": contentTypePlain,
|
||||
"accept-charset": contentTypeUnsafe,
|
||||
"action": contentTypeURL,
|
||||
"alt": contentTypePlain,
|
||||
"archive": contentTypeURL,
|
||||
"async": contentTypeUnsafe,
|
||||
"autocomplete": contentTypePlain,
|
||||
"autofocus": contentTypePlain,
|
||||
"autoplay": contentTypePlain,
|
||||
"background": contentTypeURL,
|
||||
"border": contentTypePlain,
|
||||
"checked": contentTypePlain,
|
||||
"cite": contentTypeURL,
|
||||
"challenge": contentTypeUnsafe,
|
||||
"charset": contentTypeUnsafe,
|
||||
"class": contentTypePlain,
|
||||
"classid": contentTypeURL,
|
||||
"codebase": contentTypeURL,
|
||||
"cols": contentTypePlain,
|
||||
"colspan": contentTypePlain,
|
||||
"content": contentTypeUnsafe,
|
||||
"contenteditable": contentTypePlain,
|
||||
"contextmenu": contentTypePlain,
|
||||
"controls": contentTypePlain,
|
||||
"coords": contentTypePlain,
|
||||
"crossorigin": contentTypeUnsafe,
|
||||
"data": contentTypeURL,
|
||||
"datetime": contentTypePlain,
|
||||
"default": contentTypePlain,
|
||||
"defer": contentTypeUnsafe,
|
||||
"dir": contentTypePlain,
|
||||
"dirname": contentTypePlain,
|
||||
"disabled": contentTypePlain,
|
||||
"draggable": contentTypePlain,
|
||||
"dropzone": contentTypePlain,
|
||||
"enctype": contentTypeUnsafe,
|
||||
"for": contentTypePlain,
|
||||
"form": contentTypeUnsafe,
|
||||
"formaction": contentTypeURL,
|
||||
"formenctype": contentTypeUnsafe,
|
||||
"formmethod": contentTypeUnsafe,
|
||||
"formnovalidate": contentTypeUnsafe,
|
||||
"formtarget": contentTypePlain,
|
||||
"headers": contentTypePlain,
|
||||
"height": contentTypePlain,
|
||||
"hidden": contentTypePlain,
|
||||
"high": contentTypePlain,
|
||||
"href": contentTypeURL,
|
||||
"hreflang": contentTypePlain,
|
||||
"http-equiv": contentTypeUnsafe,
|
||||
"icon": contentTypeURL,
|
||||
"id": contentTypePlain,
|
||||
"ismap": contentTypePlain,
|
||||
"keytype": contentTypeUnsafe,
|
||||
"kind": contentTypePlain,
|
||||
"label": contentTypePlain,
|
||||
"lang": contentTypePlain,
|
||||
"language": contentTypeUnsafe,
|
||||
"list": contentTypePlain,
|
||||
"longdesc": contentTypeURL,
|
||||
"loop": contentTypePlain,
|
||||
"low": contentTypePlain,
|
||||
"manifest": contentTypeURL,
|
||||
"max": contentTypePlain,
|
||||
"maxlength": contentTypePlain,
|
||||
"media": contentTypePlain,
|
||||
"mediagroup": contentTypePlain,
|
||||
"method": contentTypeUnsafe,
|
||||
"min": contentTypePlain,
|
||||
"multiple": contentTypePlain,
|
||||
"name": contentTypePlain,
|
||||
"novalidate": contentTypeUnsafe,
|
||||
// Skip handler names from
|
||||
// https://www.w3.org/TR/html5/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects
|
||||
// since we have special handling in attrType.
|
||||
"open": contentTypePlain,
|
||||
"optimum": contentTypePlain,
|
||||
"pattern": contentTypeUnsafe,
|
||||
"placeholder": contentTypePlain,
|
||||
"poster": contentTypeURL,
|
||||
"profile": contentTypeURL,
|
||||
"preload": contentTypePlain,
|
||||
"pubdate": contentTypePlain,
|
||||
"radiogroup": contentTypePlain,
|
||||
"readonly": contentTypePlain,
|
||||
"rel": contentTypeUnsafe,
|
||||
"required": contentTypePlain,
|
||||
"reversed": contentTypePlain,
|
||||
"rows": contentTypePlain,
|
||||
"rowspan": contentTypePlain,
|
||||
"sandbox": contentTypeUnsafe,
|
||||
"spellcheck": contentTypePlain,
|
||||
"scope": contentTypePlain,
|
||||
"scoped": contentTypePlain,
|
||||
"seamless": contentTypePlain,
|
||||
"selected": contentTypePlain,
|
||||
"shape": contentTypePlain,
|
||||
"size": contentTypePlain,
|
||||
"sizes": contentTypePlain,
|
||||
"span": contentTypePlain,
|
||||
"src": contentTypeURL,
|
||||
"srcdoc": contentTypeHTML,
|
||||
"srclang": contentTypePlain,
|
||||
"srcset": contentTypeSrcset,
|
||||
"start": contentTypePlain,
|
||||
"step": contentTypePlain,
|
||||
"style": contentTypeCSS,
|
||||
"tabindex": contentTypePlain,
|
||||
"target": contentTypePlain,
|
||||
"title": contentTypePlain,
|
||||
"type": contentTypeUnsafe,
|
||||
"usemap": contentTypeURL,
|
||||
"value": contentTypeUnsafe,
|
||||
"width": contentTypePlain,
|
||||
"wrap": contentTypePlain,
|
||||
"xmlns": contentTypeURL,
|
||||
}
|
||||
|
||||
// attrType returns a conservative (upper-bound on authority) guess at the
|
||||
// type of the lowercase named attribute.
|
||||
func attrType(name string) contentType {
|
||||
if strings.HasPrefix(name, "data-") {
|
||||
// Strip data- so that custom attribute heuristics below are
|
||||
// widely applied.
|
||||
// Treat data-action as URL below.
|
||||
name = name[5:]
|
||||
} else if colon := strings.IndexRune(name, ':'); colon != -1 {
|
||||
if name[:colon] == "xmlns" {
|
||||
return contentTypeURL
|
||||
}
|
||||
// Treat svg:href and xlink:href as href below.
|
||||
name = name[colon+1:]
|
||||
}
|
||||
if t, ok := attrTypeMap[name]; ok {
|
||||
return t
|
||||
}
|
||||
// Treat partial event handler names as script.
|
||||
if strings.HasPrefix(name, "on") {
|
||||
return contentTypeJS
|
||||
}
|
||||
|
||||
// Heuristics to prevent "javascript:..." injection in custom
|
||||
// data attributes and custom attributes like g:tweetUrl.
|
||||
// https://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes
|
||||
// "Custom data attributes are intended to store custom data
|
||||
// private to the page or application, for which there are no
|
||||
// more appropriate attributes or elements."
|
||||
// Developers seem to store URL content in data URLs that start
|
||||
// or end with "URI" or "URL".
|
||||
if strings.Contains(name, "src") ||
|
||||
strings.Contains(name, "uri") ||
|
||||
strings.Contains(name, "url") {
|
||||
return contentTypeURL
|
||||
}
|
||||
return contentTypePlain
|
||||
}
|
16
tpl/internal/go_templates/htmltemplate/attr_string.go
Normal file
16
tpl/internal/go_templates/htmltemplate/attr_string.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type attr"; DO NOT EDIT.
|
||||
|
||||
package template
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcset"
|
||||
|
||||
var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58}
|
||||
|
||||
func (i attr) String() string {
|
||||
if i >= attr(len(_attr_index)-1) {
|
||||
return "attr(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _attr_name[_attr_index[i]:_attr_index[i+1]]
|
||||
}
|
282
tpl/internal/go_templates/htmltemplate/clone_test.go
Normal file
282
tpl/internal/go_templates/htmltemplate/clone_test.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
func TestAddParseTree(t *testing.T) {
|
||||
root := Must(New("root").Parse(`{{define "a"}} {{.}} {{template "b"}} {{.}} "></a>{{end}}`))
|
||||
tree, err := parse.Parse("t", `{{define "b"}}<a href="{{end}}`, "", "", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added := Must(root.AddParseTree("b", tree["b"]))
|
||||
b := new(bytes.Buffer)
|
||||
err = added.ExecuteTemplate(b, "a", "1>0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := b.String(), ` 1>0 <a href=" 1%3e0 "></a>`; got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
// The {{.}} will be executed with data "<i>*/" in different contexts.
|
||||
// In the t0 template, it will be in a text context.
|
||||
// In the t1 template, it will be in a URL context.
|
||||
// In the t2 template, it will be in a JavaScript context.
|
||||
// In the t3 template, it will be in a CSS context.
|
||||
const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}`
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
// Create an incomplete template t0.
|
||||
t0 := Must(New("t0").Parse(tmpl))
|
||||
|
||||
// Clone t0 as t1.
|
||||
t1 := Must(t0.Clone())
|
||||
Must(t1.Parse(`{{define "lhs"}} <a href=" {{end}}`))
|
||||
Must(t1.Parse(`{{define "rhs"}} "></a> {{end}}`))
|
||||
|
||||
// Execute t1.
|
||||
b.Reset()
|
||||
if err := t1.ExecuteTemplate(b, "a", "<i>*/"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := b.String(), ` <a href=" %3ci%3e*/ "></a> `; got != want {
|
||||
t.Errorf("t1: got %q want %q", got, want)
|
||||
}
|
||||
|
||||
// Clone t0 as t2.
|
||||
t2 := Must(t0.Clone())
|
||||
Must(t2.Parse(`{{define "lhs"}} <p onclick="javascript: {{end}}`))
|
||||
Must(t2.Parse(`{{define "rhs"}} "></p> {{end}}`))
|
||||
|
||||
// Execute t2.
|
||||
b.Reset()
|
||||
if err := t2.ExecuteTemplate(b, "a", "<i>*/"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := b.String(), ` <p onclick="javascript: "\u003ci\u003e*/" "></p> `; got != want {
|
||||
t.Errorf("t2: got %q want %q", got, want)
|
||||
}
|
||||
|
||||
// Clone t0 as t3, but do not execute t3 yet.
|
||||
t3 := Must(t0.Clone())
|
||||
Must(t3.Parse(`{{define "lhs"}} <style> {{end}}`))
|
||||
Must(t3.Parse(`{{define "rhs"}} </style> {{end}}`))
|
||||
|
||||
// Complete t0.
|
||||
Must(t0.Parse(`{{define "lhs"}} ( {{end}}`))
|
||||
Must(t0.Parse(`{{define "rhs"}} ) {{end}}`))
|
||||
|
||||
// Clone t0 as t4. Redefining the "lhs" template should not fail.
|
||||
t4 := Must(t0.Clone())
|
||||
if _, err := t4.Parse(`{{define "lhs"}} OK {{end}}`); err != nil {
|
||||
t.Errorf(`redefine "lhs": got err %v want nil`, err)
|
||||
}
|
||||
// Cloning t1 should fail as it has been executed.
|
||||
if _, err := t1.Clone(); err == nil {
|
||||
t.Error("cloning t1: got nil err want non-nil")
|
||||
}
|
||||
// Redefining the "lhs" template in t1 should fail as it has been executed.
|
||||
if _, err := t1.Parse(`{{define "lhs"}} OK {{end}}`); err == nil {
|
||||
t.Error(`redefine "lhs": got nil err want non-nil`)
|
||||
}
|
||||
|
||||
// Execute t0.
|
||||
b.Reset()
|
||||
if err := t0.ExecuteTemplate(b, "a", "<i>*/"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := b.String(), ` ( <i>*/ ) `; got != want {
|
||||
t.Errorf("t0: got %q want %q", got, want)
|
||||
}
|
||||
|
||||
// Clone t0. This should fail, as t0 has already executed.
|
||||
if _, err := t0.Clone(); err == nil {
|
||||
t.Error(`t0.Clone(): got nil err want non-nil`)
|
||||
}
|
||||
|
||||
// Similarly, cloning sub-templates should fail.
|
||||
if _, err := t0.Lookup("a").Clone(); err == nil {
|
||||
t.Error(`t0.Lookup("a").Clone(): got nil err want non-nil`)
|
||||
}
|
||||
if _, err := t0.Lookup("lhs").Clone(); err == nil {
|
||||
t.Error(`t0.Lookup("lhs").Clone(): got nil err want non-nil`)
|
||||
}
|
||||
|
||||
// Execute t3.
|
||||
b.Reset()
|
||||
if err := t3.ExecuteTemplate(b, "a", "<i>*/"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := b.String(), ` <style> ZgotmplZ </style> `; got != want {
|
||||
t.Errorf("t3: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplates(t *testing.T) {
|
||||
names := []string{"t0", "a", "lhs", "rhs"}
|
||||
// Some template definitions borrowed from TestClone.
|
||||
const tmpl = `
|
||||
{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}
|
||||
{{define "lhs"}} <a href=" {{end}}
|
||||
{{define "rhs"}} "></a> {{end}}`
|
||||
t0 := Must(New("t0").Parse(tmpl))
|
||||
templates := t0.Templates()
|
||||
if len(templates) != len(names) {
|
||||
t.Errorf("expected %d templates; got %d", len(names), len(templates))
|
||||
}
|
||||
for _, name := range names {
|
||||
found := false
|
||||
for _, tmpl := range templates {
|
||||
if name == tmpl.text.Name() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("could not find template", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This used to crash; https://golang.org/issue/3281
|
||||
func TestCloneCrash(t *testing.T) {
|
||||
t1 := New("all")
|
||||
Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`))
|
||||
t1.Clone()
|
||||
}
|
||||
|
||||
// Ensure that this guarantee from the docs is upheld:
|
||||
// "Further calls to Parse in the copy will add templates
|
||||
// to the copy but not to the original."
|
||||
func TestCloneThenParse(t *testing.T) {
|
||||
t0 := Must(New("t0").Parse(`{{define "a"}}{{template "embedded"}}{{end}}`))
|
||||
t1 := Must(t0.Clone())
|
||||
Must(t1.Parse(`{{define "embedded"}}t1{{end}}`))
|
||||
if len(t0.Templates())+1 != len(t1.Templates()) {
|
||||
t.Error("adding a template to a clone added it to the original")
|
||||
}
|
||||
// double check that the embedded template isn't available in the original
|
||||
err := t0.ExecuteTemplate(ioutil.Discard, "a", nil)
|
||||
if err == nil {
|
||||
t.Error("expected 'no such template' error")
|
||||
}
|
||||
}
|
||||
|
||||
// https://golang.org/issue/5980
|
||||
func TestFuncMapWorksAfterClone(t *testing.T) {
|
||||
funcs := FuncMap{"customFunc": func() (string, error) {
|
||||
return "", errors.New("issue5980")
|
||||
}}
|
||||
|
||||
// get the expected error output (no clone)
|
||||
uncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))
|
||||
wantErr := uncloned.Execute(ioutil.Discard, nil)
|
||||
|
||||
// toClone must be the same as uncloned. It has to be recreated from scratch,
|
||||
// since cloning cannot occur after execution.
|
||||
toClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))
|
||||
cloned := Must(toClone.Clone())
|
||||
gotErr := cloned.Execute(ioutil.Discard, nil)
|
||||
|
||||
if wantErr.Error() != gotErr.Error() {
|
||||
t.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr)
|
||||
}
|
||||
}
|
||||
|
||||
// https://golang.org/issue/16101
|
||||
func TestTemplateCloneExecuteRace(t *testing.T) {
|
||||
const (
|
||||
input = `<title>{{block "a" .}}a{{end}}</title><body>{{block "b" .}}b{{end}}<body>`
|
||||
overlay = `{{define "b"}}A{{end}}`
|
||||
)
|
||||
outer := Must(New("outer").Parse(input))
|
||||
tmpl := Must(Must(outer.Clone()).Parse(overlay))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
if err := tmpl.Execute(ioutil.Discard, "data"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestTemplateCloneLookup(t *testing.T) {
|
||||
// Template.escape makes an assumption that the template associated
|
||||
// with t.Name() is t. Check that this holds.
|
||||
tmpl := Must(New("x").Parse("a"))
|
||||
tmpl = Must(tmpl.Clone())
|
||||
if tmpl.Lookup(tmpl.Name()) != tmpl {
|
||||
t.Error("after Clone, tmpl.Lookup(tmpl.Name()) != tmpl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneGrowth(t *testing.T) {
|
||||
tmpl := Must(New("root").Parse(`<title>{{block "B". }}Arg{{end}}</title>`))
|
||||
tmpl = Must(tmpl.Clone())
|
||||
Must(tmpl.Parse(`{{define "B"}}Text{{end}}`))
|
||||
for i := 0; i < 10; i++ {
|
||||
tmpl.Execute(ioutil.Discard, nil)
|
||||
}
|
||||
if len(tmpl.DefinedTemplates()) > 200 {
|
||||
t.Fatalf("too many templates: %v", len(tmpl.DefinedTemplates()))
|
||||
}
|
||||
}
|
||||
|
||||
// https://golang.org/issue/17735
|
||||
func TestCloneRedefinedName(t *testing.T) {
|
||||
const base = `
|
||||
{{ define "a" -}}<title>{{ template "b" . -}}</title>{{ end -}}
|
||||
{{ define "b" }}{{ end -}}
|
||||
`
|
||||
const page = `{{ template "a" . }}`
|
||||
|
||||
t1 := Must(New("a").Parse(base))
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
t2 := Must(t1.Clone())
|
||||
t2 = Must(t2.New(fmt.Sprintf("%d", i)).Parse(page))
|
||||
err := t2.Execute(ioutil.Discard, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 24791.
|
||||
func TestClonePipe(t *testing.T) {
|
||||
a := Must(New("a").Parse(`{{define "a"}}{{range $v := .A}}{{$v}}{{end}}{{end}}`))
|
||||
data := struct{ A []string }{A: []string{"hi"}}
|
||||
b := Must(a.Clone())
|
||||
var buf strings.Builder
|
||||
if err := b.Execute(&buf, &data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := buf.String(), "hi"; got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
102
tpl/internal/go_templates/htmltemplate/content.go
Normal file
102
tpl/internal/go_templates/htmltemplate/content.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
htmltemplate "html/template"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type contentType uint8
|
||||
|
||||
const (
|
||||
contentTypePlain contentType = iota
|
||||
contentTypeCSS
|
||||
contentTypeHTML
|
||||
contentTypeHTMLAttr
|
||||
contentTypeJS
|
||||
contentTypeJSStr
|
||||
contentTypeURL
|
||||
contentTypeSrcset
|
||||
// contentTypeUnsafe is used in attr.go for values that affect how
|
||||
// embedded content and network messages are formed, vetted,
|
||||
// or interpreted; or which credentials network messages carry.
|
||||
contentTypeUnsafe
|
||||
)
|
||||
|
||||
// indirect returns the value, after dereferencing as many times
|
||||
// as necessary to reach the base type (or nil).
|
||||
func indirect(a interface{}) interface{} {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
|
||||
// Avoid creating a reflect.Value if it's not a pointer.
|
||||
return a
|
||||
}
|
||||
v := reflect.ValueOf(a)
|
||||
for v.Kind() == reflect.Ptr && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
)
|
||||
|
||||
// indirectToStringerOrError returns the value, after dereferencing as many times
|
||||
// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer
|
||||
// or error,
|
||||
func indirectToStringerOrError(a interface{}) interface{} {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
v := reflect.ValueOf(a)
|
||||
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
// stringify converts its arguments to a string and the type of the content.
|
||||
// All pointers are dereferenced, as in the text/template package.
|
||||
func stringify(args ...interface{}) (string, contentType) {
|
||||
if len(args) == 1 {
|
||||
switch s := indirect(args[0]).(type) {
|
||||
case string:
|
||||
return s, contentTypePlain
|
||||
case htmltemplate.CSS:
|
||||
return string(s), contentTypeCSS
|
||||
case htmltemplate.HTML:
|
||||
return string(s), contentTypeHTML
|
||||
case htmltemplate.HTMLAttr:
|
||||
return string(s), contentTypeHTMLAttr
|
||||
case htmltemplate.JS:
|
||||
return string(s), contentTypeJS
|
||||
case htmltemplate.JSStr:
|
||||
return string(s), contentTypeJSStr
|
||||
case htmltemplate.URL:
|
||||
return string(s), contentTypeURL
|
||||
case htmltemplate.Srcset:
|
||||
return string(s), contentTypeSrcset
|
||||
}
|
||||
}
|
||||
i := 0
|
||||
for _, arg := range args {
|
||||
// We skip untyped nil arguments for backward compatibility.
|
||||
// Without this they would be output as <nil>, escaped.
|
||||
// See issue 25875.
|
||||
if arg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
args[i] = indirectToStringerOrError(arg)
|
||||
i++
|
||||
}
|
||||
return fmt.Sprint(args[:i]...), contentTypePlain
|
||||
}
|
461
tpl/internal/go_templates/htmltemplate/content_test.go
Normal file
461
tpl/internal/go_templates/htmltemplate/content_test.go
Normal file
|
@ -0,0 +1,461 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
htmltemplate "html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTypedContent(t *testing.T) {
|
||||
data := []interface{}{
|
||||
`<b> "foo%" O'Reilly &bar;`,
|
||||
htmltemplate.CSS(`a[href =~ "//example.com"]#foo`),
|
||||
htmltemplate.HTML(`Hello, <b>World</b> &tc!`),
|
||||
htmltemplate.HTMLAttr(` dir="ltr"`),
|
||||
htmltemplate.JS(`c && alert("Hello, World!");`),
|
||||
htmltemplate.JSStr(`Hello, World & O'Reilly\x21`),
|
||||
htmltemplate.URL(`greeting=H%69,&addressee=(World)`),
|
||||
htmltemplate.Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
|
||||
htmltemplate.URL(`,foo/,`),
|
||||
}
|
||||
|
||||
// For each content sensitive escaper, see how it does on
|
||||
// each of the typed strings above.
|
||||
tests := []struct {
|
||||
// A template containing a single {{.}}.
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
`<style>{{.}} { color: blue }</style>`,
|
||||
[]string{
|
||||
`ZgotmplZ`,
|
||||
// Allowed but not escaped.
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<div style="{{.}}">`,
|
||||
[]string{
|
||||
`ZgotmplZ`,
|
||||
// Allowed and HTML escaped.
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`{{.}}`,
|
||||
[]string{
|
||||
`<b> "foo%" O'Reilly &bar;`,
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
// Not escaped.
|
||||
`Hello, <b>World</b> &tc!`,
|
||||
` dir="ltr"`,
|
||||
`c && alert("Hello, World!");`,
|
||||
`Hello, World & O'Reilly\x21`,
|
||||
`greeting=H%69,&addressee=(World)`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<a{{.}}>`,
|
||||
[]string{
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
// Allowed and HTML escaped.
|
||||
` dir="ltr"`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
`ZgotmplZ`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<a title={{.}}>`,
|
||||
[]string{
|
||||
`<b> "foo%" O'Reilly &bar;`,
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
// Tags stripped, spaces escaped, entity not re-escaped.
|
||||
`Hello, World &tc!`,
|
||||
` dir="ltr"`,
|
||||
`c && alert("Hello, World!");`,
|
||||
`Hello, World & O'Reilly\x21`,
|
||||
`greeting=H%69,&addressee=(World)`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<a title='{{.}}'>`,
|
||||
[]string{
|
||||
`<b> "foo%" O'Reilly &bar;`,
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
// Tags stripped, entity not re-escaped.
|
||||
`Hello, World &tc!`,
|
||||
` dir="ltr"`,
|
||||
`c && alert("Hello, World!");`,
|
||||
`Hello, World & O'Reilly\x21`,
|
||||
`greeting=H%69,&addressee=(World)`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<textarea>{{.}}</textarea>`,
|
||||
[]string{
|
||||
`<b> "foo%" O'Reilly &bar;`,
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
// Angle brackets escaped to prevent injection of close tags, entity not re-escaped.
|
||||
`Hello, <b>World</b> &tc!`,
|
||||
` dir="ltr"`,
|
||||
`c && alert("Hello, World!");`,
|
||||
`Hello, World & O'Reilly\x21`,
|
||||
`greeting=H%69,&addressee=(World)`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<script>alert({{.}})</script>`,
|
||||
[]string{
|
||||
`"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`,
|
||||
`"a[href =~ \"//example.com\"]#foo"`,
|
||||
`"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`,
|
||||
`" dir=\"ltr\""`,
|
||||
// Not escaped.
|
||||
`c && alert("Hello, World!");`,
|
||||
// Escape sequence not over-escaped.
|
||||
`"Hello, World & O'Reilly\x21"`,
|
||||
`"greeting=H%69,\u0026addressee=(World)"`,
|
||||
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
||||
`",foo/,"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<button onclick="alert({{.}})">`,
|
||||
[]string{
|
||||
`"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`,
|
||||
`"a[href =~ \"//example.com\"]#foo"`,
|
||||
`"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`,
|
||||
`" dir=\"ltr\""`,
|
||||
// Not JS escaped but HTML escaped.
|
||||
`c && alert("Hello, World!");`,
|
||||
// Escape sequence not over-escaped.
|
||||
`"Hello, World & O'Reilly\x21"`,
|
||||
`"greeting=H%69,\u0026addressee=(World)"`,
|
||||
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
||||
`",foo/,"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<script>alert("{{.}}")</script>`,
|
||||
[]string{
|
||||
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
|
||||
`a[href =~ \x22\/\/example.com\x22]#foo`,
|
||||
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
|
||||
` dir=\x22ltr\x22`,
|
||||
`c \x26\x26 alert(\x22Hello, World!\x22);`,
|
||||
// Escape sequence not over-escaped.
|
||||
`Hello, World \x26 O\x27Reilly\x21`,
|
||||
`greeting=H%69,\x26addressee=(World)`,
|
||||
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
||||
`,foo\/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<script type="text/javascript">alert("{{.}}")</script>`,
|
||||
[]string{
|
||||
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
|
||||
`a[href =~ \x22\/\/example.com\x22]#foo`,
|
||||
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
|
||||
` dir=\x22ltr\x22`,
|
||||
`c \x26\x26 alert(\x22Hello, World!\x22);`,
|
||||
// Escape sequence not over-escaped.
|
||||
`Hello, World \x26 O\x27Reilly\x21`,
|
||||
`greeting=H%69,\x26addressee=(World)`,
|
||||
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
||||
`,foo\/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<script type="text/javascript">alert({{.}})</script>`,
|
||||
[]string{
|
||||
`"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`,
|
||||
`"a[href =~ \"//example.com\"]#foo"`,
|
||||
`"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`,
|
||||
`" dir=\"ltr\""`,
|
||||
// Not escaped.
|
||||
`c && alert("Hello, World!");`,
|
||||
// Escape sequence not over-escaped.
|
||||
`"Hello, World & O'Reilly\x21"`,
|
||||
`"greeting=H%69,\u0026addressee=(World)"`,
|
||||
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
|
||||
`",foo/,"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Not treated as JS. The output is same as for <div>{{.}}</div>
|
||||
`<script type="text/template">{{.}}</script>`,
|
||||
[]string{
|
||||
`<b> "foo%" O'Reilly &bar;`,
|
||||
`a[href =~ "//example.com"]#foo`,
|
||||
// Not escaped.
|
||||
`Hello, <b>World</b> &tc!`,
|
||||
` dir="ltr"`,
|
||||
`c && alert("Hello, World!");`,
|
||||
`Hello, World & O'Reilly\x21`,
|
||||
`greeting=H%69,&addressee=(World)`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<button onclick='alert("{{.}}")'>`,
|
||||
[]string{
|
||||
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
|
||||
`a[href =~ \x22\/\/example.com\x22]#foo`,
|
||||
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
|
||||
` dir=\x22ltr\x22`,
|
||||
`c \x26\x26 alert(\x22Hello, World!\x22);`,
|
||||
// Escape sequence not over-escaped.
|
||||
`Hello, World \x26 O\x27Reilly\x21`,
|
||||
`greeting=H%69,\x26addressee=(World)`,
|
||||
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
|
||||
`,foo\/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<a href="?q={{.}}">`,
|
||||
[]string{
|
||||
`%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b`,
|
||||
`a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo`,
|
||||
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
|
||||
`%20dir%3d%22ltr%22`,
|
||||
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
|
||||
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
|
||||
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done.
|
||||
`greeting=H%69,&addressee=%28World%29`,
|
||||
`greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<style>body { background: url('?img={{.}}') }</style>`,
|
||||
[]string{
|
||||
`%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b`,
|
||||
`a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo`,
|
||||
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
|
||||
`%20dir%3d%22ltr%22`,
|
||||
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
|
||||
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
|
||||
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done.
|
||||
`greeting=H%69,&addressee=%28World%29`,
|
||||
`greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,
|
||||
`,foo/,`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset="{{.}}">`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
// Commas are not esacped
|
||||
`Hello,#ZgotmplZ`,
|
||||
// Leading spaces are not percent escapes.
|
||||
` dir=%22ltr%22`,
|
||||
// Spaces after commas are not percent escaped.
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
// Metadata is not escaped.
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset={{.}}>`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
// Spaces are HTML escaped not %-escaped
|
||||
` dir=%22ltr%22`,
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
// Commas are escaped.
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset="{{.}} 2x, https://golang.org/ 500.5w">`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
` dir=%22ltr%22`,
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset="http://godoc.org/ {{.}}, https://golang.org/ 500.5w">`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
` dir=%22ltr%22`,
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset="http://godoc.org/?q={{.}} 2x, https://golang.org/ 500.5w">`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
` dir=%22ltr%22`,
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset="http://godoc.org/ 2x, {{.}} 500.5w">`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
` dir=%22ltr%22`,
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
{
|
||||
`<img srcset="http://godoc.org/ 2x, https://golang.org/ {{.}}">`,
|
||||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
` dir=%22ltr%22`,
|
||||
`#ZgotmplZ, World!%22%29;`,
|
||||
`Hello,#ZgotmplZ`,
|
||||
`greeting=H%69%2c&addressee=%28World%29`,
|
||||
`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
|
||||
`%2cfoo/%2c`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tmpl := Must(New("x").Parse(test.input))
|
||||
pre := strings.Index(test.input, "{{.}}")
|
||||
post := len(test.input) - (pre + 5)
|
||||
var b bytes.Buffer
|
||||
for i, x := range data {
|
||||
b.Reset()
|
||||
if err := tmpl.Execute(&b, x); err != nil {
|
||||
t.Errorf("%q with %v: %s", test.input, x, err)
|
||||
continue
|
||||
}
|
||||
if want, got := test.want[i], b.String()[pre:b.Len()-post]; want != got {
|
||||
t.Errorf("%q with %v:\nwant\n\t%q,\ngot\n\t%q\n", test.input, x, want, got)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we print using the String method. Was issue 3073.
|
||||
type stringer struct {
|
||||
v int
|
||||
}
|
||||
|
||||
func (s *stringer) String() string {
|
||||
return fmt.Sprintf("string=%d", s.v)
|
||||
}
|
||||
|
||||
type errorer struct {
|
||||
v int
|
||||
}
|
||||
|
||||
func (s *errorer) Error() string {
|
||||
return fmt.Sprintf("error=%d", s.v)
|
||||
}
|
||||
|
||||
func TestStringer(t *testing.T) {
|
||||
s := &stringer{3}
|
||||
b := new(bytes.Buffer)
|
||||
tmpl := Must(New("x").Parse("{{.}}"))
|
||||
if err := tmpl.Execute(b, s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var expect = "string=3"
|
||||
if b.String() != expect {
|
||||
t.Errorf("expected %q got %q", expect, b.String())
|
||||
}
|
||||
e := &errorer{7}
|
||||
b.Reset()
|
||||
if err := tmpl.Execute(b, e); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect = "error=7"
|
||||
if b.String() != expect {
|
||||
t.Errorf("expected %q got %q", expect, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// https://golang.org/issue/5982
|
||||
func TestEscapingNilNonemptyInterfaces(t *testing.T) {
|
||||
tmpl := Must(New("x").Parse("{{.E}}"))
|
||||
|
||||
got := new(bytes.Buffer)
|
||||
testData := struct{ E error }{} // any non-empty interface here will do; error is just ready at hand
|
||||
tmpl.Execute(got, testData)
|
||||
|
||||
// A non-empty interface should print like an empty interface.
|
||||
want := new(bytes.Buffer)
|
||||
data := struct{ E interface{} }{}
|
||||
tmpl.Execute(want, data)
|
||||
|
||||
if !bytes.Equal(want.Bytes(), got.Bytes()) {
|
||||
t.Errorf("expected %q got %q", string(want.Bytes()), string(got.Bytes()))
|
||||
}
|
||||
}
|
258
tpl/internal/go_templates/htmltemplate/context.go
Normal file
258
tpl/internal/go_templates/htmltemplate/context.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import "fmt"
|
||||
|
||||
// context describes the state an HTML parser must be in when it reaches the
|
||||
// portion of HTML produced by evaluating a particular template node.
|
||||
//
|
||||
// The zero value of type context is the start context for a template that
|
||||
// produces an HTML fragment as defined at
|
||||
// https://www.w3.org/TR/html5/syntax.html#the-end
|
||||
// where the context element is null.
|
||||
type context struct {
|
||||
state state
|
||||
delim delim
|
||||
urlPart urlPart
|
||||
jsCtx jsCtx
|
||||
attr attr
|
||||
element element
|
||||
err *Error
|
||||
}
|
||||
|
||||
func (c context) String() string {
|
||||
var err error
|
||||
if c.err != nil {
|
||||
err = c.err
|
||||
}
|
||||
return fmt.Sprintf("{%v %v %v %v %v %v %v}", c.state, c.delim, c.urlPart, c.jsCtx, c.attr, c.element, err)
|
||||
}
|
||||
|
||||
// eq reports whether two contexts are equal.
|
||||
func (c context) eq(d context) bool {
|
||||
return c.state == d.state &&
|
||||
c.delim == d.delim &&
|
||||
c.urlPart == d.urlPart &&
|
||||
c.jsCtx == d.jsCtx &&
|
||||
c.attr == d.attr &&
|
||||
c.element == d.element &&
|
||||
c.err == d.err
|
||||
}
|
||||
|
||||
// mangle produces an identifier that includes a suffix that distinguishes it
|
||||
// from template names mangled with different contexts.
|
||||
func (c context) mangle(templateName string) string {
|
||||
// The mangled name for the default context is the input templateName.
|
||||
if c.state == stateText {
|
||||
return templateName
|
||||
}
|
||||
s := templateName + "$htmltemplate_" + c.state.String()
|
||||
if c.delim != delimNone {
|
||||
s += "_" + c.delim.String()
|
||||
}
|
||||
if c.urlPart != urlPartNone {
|
||||
s += "_" + c.urlPart.String()
|
||||
}
|
||||
if c.jsCtx != jsCtxRegexp {
|
||||
s += "_" + c.jsCtx.String()
|
||||
}
|
||||
if c.attr != attrNone {
|
||||
s += "_" + c.attr.String()
|
||||
}
|
||||
if c.element != elementNone {
|
||||
s += "_" + c.element.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// state describes a high-level HTML parser state.
|
||||
//
|
||||
// It bounds the top of the element stack, and by extension the HTML insertion
|
||||
// mode, but also contains state that does not correspond to anything in the
|
||||
// HTML5 parsing algorithm because a single token production in the HTML
|
||||
// grammar may contain embedded actions in a template. For instance, the quoted
|
||||
// HTML attribute produced by
|
||||
// <div title="Hello {{.World}}">
|
||||
// is a single token in HTML's grammar but in a template spans several nodes.
|
||||
type state uint8
|
||||
|
||||
//go:generate stringer -type state
|
||||
|
||||
const (
|
||||
// stateText is parsed character data. An HTML parser is in
|
||||
// this state when its parse position is outside an HTML tag,
|
||||
// directive, comment, and special element body.
|
||||
stateText state = iota
|
||||
// stateTag occurs before an HTML attribute or the end of a tag.
|
||||
stateTag
|
||||
// stateAttrName occurs inside an attribute name.
|
||||
// It occurs between the ^'s in ` ^name^ = value`.
|
||||
stateAttrName
|
||||
// stateAfterName occurs after an attr name has ended but before any
|
||||
// equals sign. It occurs between the ^'s in ` name^ ^= value`.
|
||||
stateAfterName
|
||||
// stateBeforeValue occurs after the equals sign but before the value.
|
||||
// It occurs between the ^'s in ` name =^ ^value`.
|
||||
stateBeforeValue
|
||||
// stateHTMLCmt occurs inside an <!-- HTML comment -->.
|
||||
stateHTMLCmt
|
||||
// stateRCDATA occurs inside an RCDATA element (<textarea> or <title>)
|
||||
// as described at https://www.w3.org/TR/html5/syntax.html#elements-0
|
||||
stateRCDATA
|
||||
// stateAttr occurs inside an HTML attribute whose content is text.
|
||||
stateAttr
|
||||
// stateURL occurs inside an HTML attribute whose content is a URL.
|
||||
stateURL
|
||||
// stateSrcset occurs inside an HTML srcset attribute.
|
||||
stateSrcset
|
||||
// stateJS occurs inside an event handler or script element.
|
||||
stateJS
|
||||
// stateJSDqStr occurs inside a JavaScript double quoted string.
|
||||
stateJSDqStr
|
||||
// stateJSSqStr occurs inside a JavaScript single quoted string.
|
||||
stateJSSqStr
|
||||
// stateJSRegexp occurs inside a JavaScript regexp literal.
|
||||
stateJSRegexp
|
||||
// stateJSBlockCmt occurs inside a JavaScript /* block comment */.
|
||||
stateJSBlockCmt
|
||||
// stateJSLineCmt occurs inside a JavaScript // line comment.
|
||||
stateJSLineCmt
|
||||
// stateCSS occurs inside a <style> element or style attribute.
|
||||
stateCSS
|
||||
// stateCSSDqStr occurs inside a CSS double quoted string.
|
||||
stateCSSDqStr
|
||||
// stateCSSSqStr occurs inside a CSS single quoted string.
|
||||
stateCSSSqStr
|
||||
// stateCSSDqURL occurs inside a CSS double quoted url("...").
|
||||
stateCSSDqURL
|
||||
// stateCSSSqURL occurs inside a CSS single quoted url('...').
|
||||
stateCSSSqURL
|
||||
// stateCSSURL occurs inside a CSS unquoted url(...).
|
||||
stateCSSURL
|
||||
// stateCSSBlockCmt occurs inside a CSS /* block comment */.
|
||||
stateCSSBlockCmt
|
||||
// stateCSSLineCmt occurs inside a CSS // line comment.
|
||||
stateCSSLineCmt
|
||||
// stateError is an infectious error state outside any valid
|
||||
// HTML/CSS/JS construct.
|
||||
stateError
|
||||
)
|
||||
|
||||
// isComment is true for any state that contains content meant for template
|
||||
// authors & maintainers, not for end-users or machines.
|
||||
func isComment(s state) bool {
|
||||
switch s {
|
||||
case stateHTMLCmt, stateJSBlockCmt, stateJSLineCmt, stateCSSBlockCmt, stateCSSLineCmt:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isInTag return whether s occurs solely inside an HTML tag.
|
||||
func isInTag(s state) bool {
|
||||
switch s {
|
||||
case stateTag, stateAttrName, stateAfterName, stateBeforeValue, stateAttr:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// delim is the delimiter that will end the current HTML attribute.
|
||||
type delim uint8
|
||||
|
||||
//go:generate stringer -type delim
|
||||
|
||||
const (
|
||||
// delimNone occurs outside any attribute.
|
||||
delimNone delim = iota
|
||||
// delimDoubleQuote occurs when a double quote (") closes the attribute.
|
||||
delimDoubleQuote
|
||||
// delimSingleQuote occurs when a single quote (') closes the attribute.
|
||||
delimSingleQuote
|
||||
// delimSpaceOrTagEnd occurs when a space or right angle bracket (>)
|
||||
// closes the attribute.
|
||||
delimSpaceOrTagEnd
|
||||
)
|
||||
|
||||
// urlPart identifies a part in an RFC 3986 hierarchical URL to allow different
|
||||
// encoding strategies.
|
||||
type urlPart uint8
|
||||
|
||||
//go:generate stringer -type urlPart
|
||||
|
||||
const (
|
||||
// urlPartNone occurs when not in a URL, or possibly at the start:
|
||||
// ^ in "^http://auth/path?k=v#frag".
|
||||
urlPartNone urlPart = iota
|
||||
// urlPartPreQuery occurs in the scheme, authority, or path; between the
|
||||
// ^s in "h^ttp://auth/path^?k=v#frag".
|
||||
urlPartPreQuery
|
||||
// urlPartQueryOrFrag occurs in the query portion between the ^s in
|
||||
// "http://auth/path?^k=v#frag^".
|
||||
urlPartQueryOrFrag
|
||||
// urlPartUnknown occurs due to joining of contexts both before and
|
||||
// after the query separator.
|
||||
urlPartUnknown
|
||||
)
|
||||
|
||||
// jsCtx determines whether a '/' starts a regular expression literal or a
|
||||
// division operator.
|
||||
type jsCtx uint8
|
||||
|
||||
//go:generate stringer -type jsCtx
|
||||
|
||||
const (
|
||||
// jsCtxRegexp occurs where a '/' would start a regexp literal.
|
||||
jsCtxRegexp jsCtx = iota
|
||||
// jsCtxDivOp occurs where a '/' would start a division operator.
|
||||
jsCtxDivOp
|
||||
// jsCtxUnknown occurs where a '/' is ambiguous due to context joining.
|
||||
jsCtxUnknown
|
||||
)
|
||||
|
||||
// element identifies the HTML element when inside a start tag or special body.
|
||||
// Certain HTML element (for example <script> and <style>) have bodies that are
|
||||
// treated differently from stateText so the element type is necessary to
|
||||
// transition into the correct context at the end of a tag and to identify the
|
||||
// end delimiter for the body.
|
||||
type element uint8
|
||||
|
||||
//go:generate stringer -type element
|
||||
|
||||
const (
|
||||
// elementNone occurs outside a special tag or special element body.
|
||||
elementNone element = iota
|
||||
// elementScript corresponds to the raw text <script> element
|
||||
// with JS MIME type or no type attribute.
|
||||
elementScript
|
||||
// elementStyle corresponds to the raw text <style> element.
|
||||
elementStyle
|
||||
// elementTextarea corresponds to the RCDATA <textarea> element.
|
||||
elementTextarea
|
||||
// elementTitle corresponds to the RCDATA <title> element.
|
||||
elementTitle
|
||||
)
|
||||
|
||||
//go:generate stringer -type attr
|
||||
|
||||
// attr identifies the current HTML attribute when inside the attribute,
|
||||
// that is, starting from stateAttrName until stateTag/stateText (exclusive).
|
||||
type attr uint8
|
||||
|
||||
const (
|
||||
// attrNone corresponds to a normal attribute or no attribute.
|
||||
attrNone attr = iota
|
||||
// attrScript corresponds to an event handler attribute.
|
||||
attrScript
|
||||
// attrScriptType corresponds to the type attribute in script HTML element
|
||||
attrScriptType
|
||||
// attrStyle corresponds to the style attribute whose value is CSS.
|
||||
attrStyle
|
||||
// attrURL corresponds to an attribute whose value is a URL.
|
||||
attrURL
|
||||
// attrSrcset corresponds to a srcset attribute.
|
||||
attrSrcset
|
||||
)
|
260
tpl/internal/go_templates/htmltemplate/css.go
Normal file
260
tpl/internal/go_templates/htmltemplate/css.go
Normal file
|
@ -0,0 +1,260 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// endsWithCSSKeyword reports whether b ends with an ident that
|
||||
// case-insensitively matches the lower-case kw.
|
||||
func endsWithCSSKeyword(b []byte, kw string) bool {
|
||||
i := len(b) - len(kw)
|
||||
if i < 0 {
|
||||
// Too short.
|
||||
return false
|
||||
}
|
||||
if i != 0 {
|
||||
r, _ := utf8.DecodeLastRune(b[:i])
|
||||
if isCSSNmchar(r) {
|
||||
// Too long.
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Many CSS keywords, such as "!important" can have characters encoded,
|
||||
// but the URI production does not allow that according to
|
||||
// https://www.w3.org/TR/css3-syntax/#TOK-URI
|
||||
// This does not attempt to recognize encoded keywords. For example,
|
||||
// given "\75\72\6c" and "url" this return false.
|
||||
return string(bytes.ToLower(b[i:])) == kw
|
||||
}
|
||||
|
||||
// isCSSNmchar reports whether rune is allowed anywhere in a CSS identifier.
|
||||
func isCSSNmchar(r rune) bool {
|
||||
// Based on the CSS3 nmchar production but ignores multi-rune escape
|
||||
// sequences.
|
||||
// https://www.w3.org/TR/css3-syntax/#SUBTOK-nmchar
|
||||
return 'a' <= r && r <= 'z' ||
|
||||
'A' <= r && r <= 'Z' ||
|
||||
'0' <= r && r <= '9' ||
|
||||
r == '-' ||
|
||||
r == '_' ||
|
||||
// Non-ASCII cases below.
|
||||
0x80 <= r && r <= 0xd7ff ||
|
||||
0xe000 <= r && r <= 0xfffd ||
|
||||
0x10000 <= r && r <= 0x10ffff
|
||||
}
|
||||
|
||||
// decodeCSS decodes CSS3 escapes given a sequence of stringchars.
|
||||
// If there is no change, it returns the input, otherwise it returns a slice
|
||||
// backed by a new array.
|
||||
// https://www.w3.org/TR/css3-syntax/#SUBTOK-stringchar defines stringchar.
|
||||
func decodeCSS(s []byte) []byte {
|
||||
i := bytes.IndexByte(s, '\\')
|
||||
if i == -1 {
|
||||
return s
|
||||
}
|
||||
// The UTF-8 sequence for a codepoint is never longer than 1 + the
|
||||
// number hex digits need to represent that codepoint, so len(s) is an
|
||||
// upper bound on the output length.
|
||||
b := make([]byte, 0, len(s))
|
||||
for len(s) != 0 {
|
||||
i := bytes.IndexByte(s, '\\')
|
||||
if i == -1 {
|
||||
i = len(s)
|
||||
}
|
||||
b, s = append(b, s[:i]...), s[i:]
|
||||
if len(s) < 2 {
|
||||
break
|
||||
}
|
||||
// https://www.w3.org/TR/css3-syntax/#SUBTOK-escape
|
||||
// escape ::= unicode | '\' [#x20-#x7E#x80-#xD7FF#xE000-#xFFFD#x10000-#x10FFFF]
|
||||
if isHex(s[1]) {
|
||||
// https://www.w3.org/TR/css3-syntax/#SUBTOK-unicode
|
||||
// unicode ::= '\' [0-9a-fA-F]{1,6} wc?
|
||||
j := 2
|
||||
for j < len(s) && j < 7 && isHex(s[j]) {
|
||||
j++
|
||||
}
|
||||
r := hexDecode(s[1:j])
|
||||
if r > unicode.MaxRune {
|
||||
r, j = r/16, j-1
|
||||
}
|
||||
n := utf8.EncodeRune(b[len(b):cap(b)], r)
|
||||
// The optional space at the end allows a hex
|
||||
// sequence to be followed by a literal hex.
|
||||
// string(decodeCSS([]byte(`\A B`))) == "\nB"
|
||||
b, s = b[:len(b)+n], skipCSSSpace(s[j:])
|
||||
} else {
|
||||
// `\\` decodes to `\` and `\"` to `"`.
|
||||
_, n := utf8.DecodeRune(s[1:])
|
||||
b, s = append(b, s[1:1+n]...), s[1+n:]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// isHex reports whether the given character is a hex digit.
|
||||
func isHex(c byte) bool {
|
||||
return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
|
||||
}
|
||||
|
||||
// hexDecode decodes a short hex digit sequence: "10" -> 16.
|
||||
func hexDecode(s []byte) rune {
|
||||
n := '\x00'
|
||||
for _, c := range s {
|
||||
n <<= 4
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
n |= rune(c - '0')
|
||||
case 'a' <= c && c <= 'f':
|
||||
n |= rune(c-'a') + 10
|
||||
case 'A' <= c && c <= 'F':
|
||||
n |= rune(c-'A') + 10
|
||||
default:
|
||||
panic(fmt.Sprintf("Bad hex digit in %q", s))
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// skipCSSSpace returns a suffix of c, skipping over a single space.
|
||||
func skipCSSSpace(c []byte) []byte {
|
||||
if len(c) == 0 {
|
||||
return c
|
||||
}
|
||||
// wc ::= #x9 | #xA | #xC | #xD | #x20
|
||||
switch c[0] {
|
||||
case '\t', '\n', '\f', ' ':
|
||||
return c[1:]
|
||||
case '\r':
|
||||
// This differs from CSS3's wc production because it contains a
|
||||
// probable spec error whereby wc contains all the single byte
|
||||
// sequences in nl (newline) but not CRLF.
|
||||
if len(c) >= 2 && c[1] == '\n' {
|
||||
return c[2:]
|
||||
}
|
||||
return c[1:]
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// isCSSSpace reports whether b is a CSS space char as defined in wc.
|
||||
func isCSSSpace(b byte) bool {
|
||||
switch b {
|
||||
case '\t', '\n', '\f', '\r', ' ':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes.
|
||||
func cssEscaper(args ...interface{}) string {
|
||||
s, _ := stringify(args...)
|
||||
var b strings.Builder
|
||||
r, w, written := rune(0), 0, 0
|
||||
for i := 0; i < len(s); i += w {
|
||||
// See comment in htmlEscaper.
|
||||
r, w = utf8.DecodeRuneInString(s[i:])
|
||||
var repl string
|
||||
switch {
|
||||
case int(r) < len(cssReplacementTable) && cssReplacementTable[r] != "":
|
||||
repl = cssReplacementTable[r]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if written == 0 {
|
||||
b.Grow(len(s))
|
||||
}
|
||||
b.WriteString(s[written:i])
|
||||
b.WriteString(repl)
|
||||
written = i + w
|
||||
if repl != `\\` && (written == len(s) || isHex(s[written]) || isCSSSpace(s[written])) {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
if written == 0 {
|
||||
return s
|
||||
}
|
||||
b.WriteString(s[written:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var cssReplacementTable = []string{
|
||||
0: `\0`,
|
||||
'\t': `\9`,
|
||||
'\n': `\a`,
|
||||
'\f': `\c`,
|
||||
'\r': `\d`,
|
||||
// Encode HTML specials as hex so the output can be embedded
|
||||
// in HTML attributes without further encoding.
|
||||
'"': `\22`,
|
||||
'&': `\26`,
|
||||
'\'': `\27`,
|
||||
'(': `\28`,
|
||||
')': `\29`,
|
||||
'+': `\2b`,
|
||||
'/': `\2f`,
|
||||
':': `\3a`,
|
||||
';': `\3b`,
|
||||
'<': `\3c`,
|
||||
'>': `\3e`,
|
||||
'\\': `\\`,
|
||||
'{': `\7b`,
|
||||
'}': `\7d`,
|
||||
}
|
||||
|
||||
var expressionBytes = []byte("expression")
|
||||
var mozBindingBytes = []byte("mozbinding")
|
||||
|
||||
// cssValueFilter allows innocuous CSS values in the output including CSS
|
||||
// quantities (10px or 25%), ID or class literals (#foo, .bar), keyword values
|
||||
// (inherit, blue), and colors (#888).
|
||||
// It filters out unsafe values, such as those that affect token boundaries,
|
||||
// and anything that might execute scripts.
|
||||
func cssValueFilter(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeCSS {
|
||||
return s
|
||||
}
|
||||
b, id := decodeCSS([]byte(s)), make([]byte, 0, 64)
|
||||
|
||||
// CSS3 error handling is specified as honoring string boundaries per
|
||||
// https://www.w3.org/TR/css3-syntax/#error-handling :
|
||||
// Malformed declarations. User agents must handle unexpected
|
||||
// tokens encountered while parsing a declaration by reading until
|
||||
// the end of the declaration, while observing the rules for
|
||||
// matching pairs of (), [], {}, "", and '', and correctly handling
|
||||
// escapes. For example, a malformed declaration may be missing a
|
||||
// property, colon (:) or value.
|
||||
// So we need to make sure that values do not have mismatched bracket
|
||||
// or quote characters to prevent the browser from restarting parsing
|
||||
// inside a string that might embed JavaScript source.
|
||||
for i, c := range b {
|
||||
switch c {
|
||||
case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}':
|
||||
return filterFailsafe
|
||||
case '-':
|
||||
// Disallow <!-- or -->.
|
||||
// -- should not appear in valid identifiers.
|
||||
if i != 0 && b[i-1] == '-' {
|
||||
return filterFailsafe
|
||||
}
|
||||
default:
|
||||
if c < utf8.RuneSelf && isCSSNmchar(rune(c)) {
|
||||
id = append(id, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
id = bytes.ToLower(id)
|
||||
if bytes.Contains(id, expressionBytes) || bytes.Contains(id, mozBindingBytes) {
|
||||
return filterFailsafe
|
||||
}
|
||||
return string(b)
|
||||
}
|
283
tpl/internal/go_templates/htmltemplate/css_test.go
Normal file
283
tpl/internal/go_templates/htmltemplate/css_test.go
Normal file
|
@ -0,0 +1,283 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEndsWithCSSKeyword(t *testing.T) {
|
||||
tests := []struct {
|
||||
css, kw string
|
||||
want bool
|
||||
}{
|
||||
{"", "url", false},
|
||||
{"url", "url", true},
|
||||
{"URL", "url", true},
|
||||
{"Url", "url", true},
|
||||
{"url", "important", false},
|
||||
{"important", "important", true},
|
||||
{"image-url", "url", false},
|
||||
{"imageurl", "url", false},
|
||||
{"image url", "url", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := endsWithCSSKeyword([]byte(test.css), test.kw)
|
||||
if got != test.want {
|
||||
t.Errorf("want %t but got %t for css=%v, kw=%v", test.want, got, test.css, test.kw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCSSNmchar(t *testing.T) {
|
||||
tests := []struct {
|
||||
rune rune
|
||||
want bool
|
||||
}{
|
||||
{0, false},
|
||||
{'0', true},
|
||||
{'9', true},
|
||||
{'A', true},
|
||||
{'Z', true},
|
||||
{'a', true},
|
||||
{'z', true},
|
||||
{'_', true},
|
||||
{'-', true},
|
||||
{':', false},
|
||||
{';', false},
|
||||
{' ', false},
|
||||
{0x7f, false},
|
||||
{0x80, true},
|
||||
{0x1234, true},
|
||||
{0xd800, false},
|
||||
{0xdc00, false},
|
||||
{0xfffe, false},
|
||||
{0x10000, true},
|
||||
{0x110000, false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := isCSSNmchar(test.rune)
|
||||
if got != test.want {
|
||||
t.Errorf("%q: want %t but got %t", string(test.rune), test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeCSS(t *testing.T) {
|
||||
tests := []struct {
|
||||
css, want string
|
||||
}{
|
||||
{``, ``},
|
||||
{`foo`, `foo`},
|
||||
{`foo\`, `foo`},
|
||||
{`foo\\`, `foo\`},
|
||||
{`\`, ``},
|
||||
{`\A`, "\n"},
|
||||
{`\a`, "\n"},
|
||||
{`\0a`, "\n"},
|
||||
{`\00000a`, "\n"},
|
||||
{`\000000a`, "\u0000a"},
|
||||
{`\1234 5`, "\u1234" + "5"},
|
||||
{`\1234\20 5`, "\u1234" + " 5"},
|
||||
{`\1234\A 5`, "\u1234" + "\n5"},
|
||||
{"\\1234\t5", "\u1234" + "5"},
|
||||
{"\\1234\n5", "\u1234" + "5"},
|
||||
{"\\1234\r\n5", "\u1234" + "5"},
|
||||
{`\12345`, "\U00012345"},
|
||||
{`\\`, `\`},
|
||||
{`\\ `, `\ `},
|
||||
{`\"`, `"`},
|
||||
{`\'`, `'`},
|
||||
{`\.`, `.`},
|
||||
{`\. .`, `. .`},
|
||||
{
|
||||
`The \3c i\3equick\3c/i\3e,\d\A\3cspan style=\27 color:brown\27\3e brown\3c/span\3e fox jumps\2028over the \3c canine class=\22lazy\22 \3e dog\3c/canine\3e`,
|
||||
"The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got1 := string(decodeCSS([]byte(test.css)))
|
||||
if got1 != test.want {
|
||||
t.Errorf("%q: want\n\t%q\nbut got\n\t%q", test.css, test.want, got1)
|
||||
}
|
||||
recoded := cssEscaper(got1)
|
||||
if got2 := string(decodeCSS([]byte(recoded))); got2 != test.want {
|
||||
t.Errorf("%q: escape & decode not dual for %q", test.css, recoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexDecode(t *testing.T) {
|
||||
for i := 0; i < 0x200000; i += 101 /* coprime with 16 */ {
|
||||
s := strconv.FormatInt(int64(i), 16)
|
||||
if got := int(hexDecode([]byte(s))); got != i {
|
||||
t.Errorf("%s: want %d but got %d", s, i, got)
|
||||
}
|
||||
s = strings.ToUpper(s)
|
||||
if got := int(hexDecode([]byte(s))); got != i {
|
||||
t.Errorf("%s: want %d but got %d", s, i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipCSSSpace(t *testing.T) {
|
||||
tests := []struct {
|
||||
css, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"foo", "foo"},
|
||||
{"\n", ""},
|
||||
{"\r\n", ""},
|
||||
{"\r", ""},
|
||||
{"\t", ""},
|
||||
{" ", ""},
|
||||
{"\f", ""},
|
||||
{" foo", "foo"},
|
||||
{" foo", " foo"},
|
||||
{`\20`, `\20`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := string(skipCSSSpace([]byte(test.css)))
|
||||
if got != test.want {
|
||||
t.Errorf("%q: want %q but got %q", test.css, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSSEscaper(t *testing.T) {
|
||||
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !"#$%&'()*+,-./` +
|
||||
`0123456789:;<=>?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\]^_` +
|
||||
"`abcdefghijklmno" +
|
||||
"pqrstuvwxyz{|}~\x7f" +
|
||||
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
|
||||
|
||||
want := ("\\0\x01\x02\x03\x04\x05\x06\x07" +
|
||||
"\x08\\9 \\a\x0b\\c \\d\x0E\x0F" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17" +
|
||||
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !\22#$%\26\27\28\29*\2b,-.\2f ` +
|
||||
`0123456789\3a\3b\3c=\3e?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\\]^_` +
|
||||
"`abcdefghijklmno" +
|
||||
`pqrstuvwxyz\7b|\7d~` + "\u007f" +
|
||||
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
|
||||
|
||||
got := cssEscaper(input)
|
||||
if got != want {
|
||||
t.Errorf("encode: want\n\t%q\nbut got\n\t%q", want, got)
|
||||
}
|
||||
|
||||
got = string(decodeCSS([]byte(got)))
|
||||
if input != got {
|
||||
t.Errorf("decode: want\n\t%q\nbut got\n\t%q", input, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSSValueFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
css, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"foo", "foo"},
|
||||
{"0", "0"},
|
||||
{"0px", "0px"},
|
||||
{"-5px", "-5px"},
|
||||
{"1.25in", "1.25in"},
|
||||
{"+.33em", "+.33em"},
|
||||
{"100%", "100%"},
|
||||
{"12.5%", "12.5%"},
|
||||
{".foo", ".foo"},
|
||||
{"#bar", "#bar"},
|
||||
{"corner-radius", "corner-radius"},
|
||||
{"-moz-corner-radius", "-moz-corner-radius"},
|
||||
{"#000", "#000"},
|
||||
{"#48f", "#48f"},
|
||||
{"#123456", "#123456"},
|
||||
{"U+00-FF, U+980-9FF", "U+00-FF, U+980-9FF"},
|
||||
{"color: red", "color: red"},
|
||||
{"<!--", "ZgotmplZ"},
|
||||
{"-->", "ZgotmplZ"},
|
||||
{"<![CDATA[", "ZgotmplZ"},
|
||||
{"]]>", "ZgotmplZ"},
|
||||
{"</style", "ZgotmplZ"},
|
||||
{`"`, "ZgotmplZ"},
|
||||
{`'`, "ZgotmplZ"},
|
||||
{"`", "ZgotmplZ"},
|
||||
{"\x00", "ZgotmplZ"},
|
||||
{"/* foo */", "ZgotmplZ"},
|
||||
{"//", "ZgotmplZ"},
|
||||
{"[href=~", "ZgotmplZ"},
|
||||
{"expression(alert(1337))", "ZgotmplZ"},
|
||||
{"-expression(alert(1337))", "ZgotmplZ"},
|
||||
{"expression", "ZgotmplZ"},
|
||||
{"Expression", "ZgotmplZ"},
|
||||
{"EXPRESSION", "ZgotmplZ"},
|
||||
{"-moz-binding", "ZgotmplZ"},
|
||||
{"-expr\x00ession(alert(1337))", "ZgotmplZ"},
|
||||
{`-expr\0ession(alert(1337))`, "ZgotmplZ"},
|
||||
{`-express\69on(alert(1337))`, "ZgotmplZ"},
|
||||
{`-express\69 on(alert(1337))`, "ZgotmplZ"},
|
||||
{`-exp\72 ession(alert(1337))`, "ZgotmplZ"},
|
||||
{`-exp\52 ession(alert(1337))`, "ZgotmplZ"},
|
||||
{`-exp\000052 ession(alert(1337))`, "ZgotmplZ"},
|
||||
{`-expre\0000073sion`, "-expre\x073sion"},
|
||||
{`@import url evil.css`, "ZgotmplZ"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := cssValueFilter(test.css)
|
||||
if got != test.want {
|
||||
t.Errorf("%q: want %q but got %q", test.css, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCSSEscaper(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cssEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCSSEscaperNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cssEscaper("The quick, brown fox jumps over the lazy dog.")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecodeCSS(b *testing.B) {
|
||||
s := []byte(`The \3c i\3equick\3c/i\3e,\d\A\3cspan style=\27 color:brown\27\3e brown\3c/span\3e fox jumps\2028over the \3c canine class=\22lazy\22 \3edog\3c/canine\3e`)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
decodeCSS(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecodeCSSNoSpecials(b *testing.B) {
|
||||
s := []byte("The quick, brown fox jumps over the lazy dog.")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
decodeCSS(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCSSValueFilter(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cssValueFilter(` e\78preS\0Sio/**/n(alert(1337))`)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCSSValueFilterOk(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cssValueFilter(`Times New Roman`)
|
||||
}
|
||||
}
|
16
tpl/internal/go_templates/htmltemplate/delim_string.go
Normal file
16
tpl/internal/go_templates/htmltemplate/delim_string.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type delim"; DO NOT EDIT.
|
||||
|
||||
package template
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _delim_name = "delimNonedelimDoubleQuotedelimSingleQuotedelimSpaceOrTagEnd"
|
||||
|
||||
var _delim_index = [...]uint8{0, 9, 25, 41, 59}
|
||||
|
||||
func (i delim) String() string {
|
||||
if i >= delim(len(_delim_index)-1) {
|
||||
return "delim(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _delim_name[_delim_index[i]:_delim_index[i+1]]
|
||||
}
|
196
tpl/internal/go_templates/htmltemplate/doc.go
Normal file
196
tpl/internal/go_templates/htmltemplate/doc.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2011 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 template (html/template) implements data-driven templates for
|
||||
generating HTML output safe against code injection. It provides the
|
||||
same interface as package text/template and should be used instead of
|
||||
text/template whenever the output is HTML.
|
||||
|
||||
The documentation here focuses on the security features of the package.
|
||||
For information about how to program the templates themselves, see the
|
||||
documentation for text/template.
|
||||
|
||||
Introduction
|
||||
|
||||
This package wraps package text/template so you can share its template API
|
||||
to parse and execute HTML templates safely.
|
||||
|
||||
tmpl, err := template.New("name").Parse(...)
|
||||
// Error checking elided
|
||||
err = tmpl.Execute(out, data)
|
||||
|
||||
If successful, tmpl will now be injection-safe. Otherwise, err is an error
|
||||
defined in the docs for ErrorCode.
|
||||
|
||||
HTML templates treat data values as plain text which should be encoded so they
|
||||
can be safely embedded in an HTML document. The escaping is contextual, so
|
||||
actions can appear within JavaScript, CSS, and URI contexts.
|
||||
|
||||
The security model used by this package assumes that template authors are
|
||||
trusted, while Execute's data parameter is not. More details are
|
||||
provided below.
|
||||
|
||||
Example
|
||||
|
||||
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
produces
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
but the contextual autoescaping in html/template
|
||||
|
||||
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
produces safe, escaped HTML output
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
|
||||
Contexts
|
||||
|
||||
This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
|
||||
functions to each simple action pipeline, so given the excerpt
|
||||
|
||||
<a href="/search?q={{.}}">{{.}}</a>
|
||||
|
||||
At parse time each {{.}} is overwritten to add escaping functions as necessary.
|
||||
In this case it becomes
|
||||
|
||||
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
|
||||
|
||||
where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping
|
||||
functions.
|
||||
|
||||
For these internal escaping functions, if an action pipeline evaluates to
|
||||
a nil interface value, it is treated as though it were an empty string.
|
||||
|
||||
Errors
|
||||
|
||||
See the documentation of ErrorCode for details.
|
||||
|
||||
|
||||
A fuller picture
|
||||
|
||||
The rest of this package comment may be skipped on first reading; it includes
|
||||
details necessary to understand escaping contexts and error messages. Most users
|
||||
will not need to understand these details.
|
||||
|
||||
|
||||
Contexts
|
||||
|
||||
Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows
|
||||
how {{.}} appears when used in the context to the left.
|
||||
|
||||
Context {{.}} After
|
||||
{{.}} O'Reilly: How are <i>you</i>?
|
||||
<a title='{{.}}'> O'Reilly: How are you?
|
||||
<a href="/{{.}}"> O'Reilly: How are %3ci%3eyou%3c/i%3e?
|
||||
<a href="?q={{.}}"> O'Reilly%3a%20How%20are%3ci%3e...%3f
|
||||
<a onx='f("{{.}}")'> O\x27Reilly: How are \x3ci\x3eyou...?
|
||||
<a onx='f({{.}})'> "O\x27Reilly: How are \x3ci\x3eyou...?"
|
||||
<a onx='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f
|
||||
|
||||
If used in an unsafe context, then the value might be filtered out:
|
||||
|
||||
Context {{.}} After
|
||||
<a href="{{.}}"> #ZgotmplZ
|
||||
|
||||
since "O'Reilly:" is not an allowed protocol like "http:".
|
||||
|
||||
|
||||
If {{.}} is the innocuous word, `left`, then it can appear more widely,
|
||||
|
||||
Context {{.}} After
|
||||
{{.}} left
|
||||
<a title='{{.}}'> left
|
||||
<a href='{{.}}'> left
|
||||
<a href='/{{.}}'> left
|
||||
<a href='?dir={{.}}'> left
|
||||
<a style="border-{{.}}: 4px"> left
|
||||
<a style="align: {{.}}"> left
|
||||
<a style="background: '{{.}}'> left
|
||||
<a style="background: url('{{.}}')> left
|
||||
<style>p.{{.}} {color:red}</style> left
|
||||
|
||||
Non-string values can be used in JavaScript contexts.
|
||||
If {{.}} is
|
||||
|
||||
struct{A,B string}{ "foo", "bar" }
|
||||
|
||||
in the escaped template
|
||||
|
||||
<script>var pair = {{.}};</script>
|
||||
|
||||
then the template output is
|
||||
|
||||
<script>var pair = {"A": "foo", "B": "bar"};</script>
|
||||
|
||||
See package json to understand how non-string content is marshaled for
|
||||
embedding in JavaScript contexts.
|
||||
|
||||
|
||||
Typed Strings
|
||||
|
||||
By default, this package assumes that all pipelines produce a plain text string.
|
||||
It adds escaping pipeline stages necessary to correctly and safely embed that
|
||||
plain text string in the appropriate context.
|
||||
|
||||
When a data value is not plain text, you can make sure it is not over-escaped
|
||||
by marking it with its type.
|
||||
|
||||
Types HTML, JS, URL, and others from content.go can carry safe content that is
|
||||
exempted from escaping.
|
||||
|
||||
The template
|
||||
|
||||
Hello, {{.}}!
|
||||
|
||||
can be invoked with
|
||||
|
||||
tmpl.Execute(out, template.HTML(`<b>World</b>`))
|
||||
|
||||
to produce
|
||||
|
||||
Hello, <b>World</b>!
|
||||
|
||||
instead of the
|
||||
|
||||
Hello, <b>World<b>!
|
||||
|
||||
that would have been produced if {{.}} was a regular string.
|
||||
|
||||
|
||||
Security Model
|
||||
|
||||
https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition defines "safe" as used by this package.
|
||||
|
||||
This package assumes that template authors are trusted, that Execute's data
|
||||
parameter is not, and seeks to preserve the properties below in the face
|
||||
of untrusted data:
|
||||
|
||||
Structure Preservation Property:
|
||||
"... when a template author writes an HTML tag in a safe templating language,
|
||||
the browser will interpret the corresponding portion of the output as a tag
|
||||
regardless of the values of untrusted data, and similarly for other structures
|
||||
such as attribute boundaries and JS and CSS string boundaries."
|
||||
|
||||
Code Effect Property:
|
||||
"... only code specified by the template author should run as a result of
|
||||
injecting the template output into a page and all code specified by the
|
||||
template author should run as a result of the same."
|
||||
|
||||
Least Surprise Property:
|
||||
"A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who
|
||||
knows that contextual autoescaping happens should be able to look at a {{.}}
|
||||
and correctly infer what sanitization happens."
|
||||
*/
|
||||
package template
|
16
tpl/internal/go_templates/htmltemplate/element_string.go
Normal file
16
tpl/internal/go_templates/htmltemplate/element_string.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type element"; DO NOT EDIT.
|
||||
|
||||
package template
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _element_name = "elementNoneelementScriptelementStyleelementTextareaelementTitle"
|
||||
|
||||
var _element_index = [...]uint8{0, 11, 24, 36, 51, 63}
|
||||
|
||||
func (i element) String() string {
|
||||
if i >= element(len(_element_index)-1) {
|
||||
return "element(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _element_name[_element_index[i]:_element_index[i+1]]
|
||||
}
|
234
tpl/internal/go_templates/htmltemplate/error.go
Normal file
234
tpl/internal/go_templates/htmltemplate/error.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
// Error describes a problem encountered during template Escaping.
|
||||
type Error struct {
|
||||
// ErrorCode describes the kind of error.
|
||||
ErrorCode ErrorCode
|
||||
// Node is the node that caused the problem, if known.
|
||||
// If not nil, it overrides Name and Line.
|
||||
Node parse.Node
|
||||
// Name is the name of the template in which the error was encountered.
|
||||
Name string
|
||||
// Line is the line number of the error in the template source or 0.
|
||||
Line int
|
||||
// Description is a human-readable description of the problem.
|
||||
Description string
|
||||
}
|
||||
|
||||
// ErrorCode is a code for a kind of error.
|
||||
type ErrorCode int
|
||||
|
||||
// We define codes for each error that manifests while escaping templates, but
|
||||
// escaped templates may also fail at runtime.
|
||||
//
|
||||
// Output: "ZgotmplZ"
|
||||
// Example:
|
||||
// <img src="{{.X}}">
|
||||
// where {{.X}} evaluates to `javascript:...`
|
||||
// Discussion:
|
||||
// "ZgotmplZ" is a special value that indicates that unsafe content reached a
|
||||
// CSS or URL context at runtime. The output of the example will be
|
||||
// <img src="#ZgotmplZ">
|
||||
// If the data comes from a trusted source, use content types to exempt it
|
||||
// from filtering: URL(`javascript:...`).
|
||||
const (
|
||||
// OK indicates the lack of an error.
|
||||
OK ErrorCode = iota
|
||||
|
||||
// ErrAmbigContext: "... appears in an ambiguous context within a URL"
|
||||
// Example:
|
||||
// <a href="
|
||||
// {{if .C}}
|
||||
// /path/
|
||||
// {{else}}
|
||||
// /search?q=
|
||||
// {{end}}
|
||||
// {{.X}}
|
||||
// ">
|
||||
// Discussion:
|
||||
// {{.X}} is in an ambiguous URL context since, depending on {{.C}},
|
||||
// it may be either a URL suffix or a query parameter.
|
||||
// Moving {{.X}} into the condition removes the ambiguity:
|
||||
// <a href="{{if .C}}/path/{{.X}}{{else}}/search?q={{.X}}">
|
||||
ErrAmbigContext
|
||||
|
||||
// ErrBadHTML: "expected space, attr name, or end of tag, but got ...",
|
||||
// "... in unquoted attr", "... in attribute name"
|
||||
// Example:
|
||||
// <a href = /search?q=foo>
|
||||
// <href=foo>
|
||||
// <form na<e=...>
|
||||
// <option selected<
|
||||
// Discussion:
|
||||
// This is often due to a typo in an HTML element, but some runes
|
||||
// are banned in tag names, attribute names, and unquoted attribute
|
||||
// values because they can tickle parser ambiguities.
|
||||
// Quoting all attributes is the best policy.
|
||||
ErrBadHTML
|
||||
|
||||
// ErrBranchEnd: "{{if}} branches end in different contexts"
|
||||
// Example:
|
||||
// {{if .C}}<a href="{{end}}{{.X}}
|
||||
// Discussion:
|
||||
// Package html/template statically examines each path through an
|
||||
// {{if}}, {{range}}, or {{with}} to escape any following pipelines.
|
||||
// The example is ambiguous since {{.X}} might be an HTML text node,
|
||||
// or a URL prefix in an HTML attribute. The context of {{.X}} is
|
||||
// used to figure out how to escape it, but that context depends on
|
||||
// the run-time value of {{.C}} which is not statically known.
|
||||
//
|
||||
// The problem is usually something like missing quotes or angle
|
||||
// brackets, or can be avoided by refactoring to put the two contexts
|
||||
// into different branches of an if, range or with. If the problem
|
||||
// is in a {{range}} over a collection that should never be empty,
|
||||
// adding a dummy {{else}} can help.
|
||||
ErrBranchEnd
|
||||
|
||||
// ErrEndContext: "... ends in a non-text context: ..."
|
||||
// Examples:
|
||||
// <div
|
||||
// <div title="no close quote>
|
||||
// <script>f()
|
||||
// Discussion:
|
||||
// Executed templates should produce a DocumentFragment of HTML.
|
||||
// Templates that end without closing tags will trigger this error.
|
||||
// Templates that should not be used in an HTML context or that
|
||||
// produce incomplete Fragments should not be executed directly.
|
||||
//
|
||||
// {{define "main"}} <script>{{template "helper"}}</script> {{end}}
|
||||
// {{define "helper"}} document.write(' <div title=" ') {{end}}
|
||||
//
|
||||
// "helper" does not produce a valid document fragment, so should
|
||||
// not be Executed directly.
|
||||
ErrEndContext
|
||||
|
||||
// ErrNoSuchTemplate: "no such template ..."
|
||||
// Examples:
|
||||
// {{define "main"}}<div {{template "attrs"}}>{{end}}
|
||||
// {{define "attrs"}}href="{{.URL}}"{{end}}
|
||||
// Discussion:
|
||||
// Package html/template looks through template calls to compute the
|
||||
// context.
|
||||
// Here the {{.URL}} in "attrs" must be treated as a URL when called
|
||||
// from "main", but you will get this error if "attrs" is not defined
|
||||
// when "main" is parsed.
|
||||
ErrNoSuchTemplate
|
||||
|
||||
// ErrOutputContext: "cannot compute output context for template ..."
|
||||
// Examples:
|
||||
// {{define "t"}}{{if .T}}{{template "t" .T}}{{end}}{{.H}}",{{end}}
|
||||
// Discussion:
|
||||
// A recursive template does not end in the same context in which it
|
||||
// starts, and a reliable output context cannot be computed.
|
||||
// Look for typos in the named template.
|
||||
// If the template should not be called in the named start context,
|
||||
// look for calls to that template in unexpected contexts.
|
||||
// Maybe refactor recursive templates to not be recursive.
|
||||
ErrOutputContext
|
||||
|
||||
// ErrPartialCharset: "unfinished JS regexp charset in ..."
|
||||
// Example:
|
||||
// <script>var pattern = /foo[{{.Chars}}]/</script>
|
||||
// Discussion:
|
||||
// Package html/template does not support interpolation into regular
|
||||
// expression literal character sets.
|
||||
ErrPartialCharset
|
||||
|
||||
// ErrPartialEscape: "unfinished escape sequence in ..."
|
||||
// Example:
|
||||
// <script>alert("\{{.X}}")</script>
|
||||
// Discussion:
|
||||
// Package html/template does not support actions following a
|
||||
// backslash.
|
||||
// This is usually an error and there are better solutions; for
|
||||
// example
|
||||
// <script>alert("{{.X}}")</script>
|
||||
// should work, and if {{.X}} is a partial escape sequence such as
|
||||
// "xA0", mark the whole sequence as safe content: JSStr(`\xA0`)
|
||||
ErrPartialEscape
|
||||
|
||||
// ErrRangeLoopReentry: "on range loop re-entry: ..."
|
||||
// Example:
|
||||
// <script>var x = [{{range .}}'{{.}},{{end}}]</script>
|
||||
// Discussion:
|
||||
// If an iteration through a range would cause it to end in a
|
||||
// different context than an earlier pass, there is no single context.
|
||||
// In the example, there is missing a quote, so it is not clear
|
||||
// whether {{.}} is meant to be inside a JS string or in a JS value
|
||||
// context. The second iteration would produce something like
|
||||
//
|
||||
// <script>var x = ['firstValue,'secondValue]</script>
|
||||
ErrRangeLoopReentry
|
||||
|
||||
// ErrSlashAmbig: '/' could start a division or regexp.
|
||||
// Example:
|
||||
// <script>
|
||||
// {{if .C}}var x = 1{{end}}
|
||||
// /-{{.N}}/i.test(x) ? doThis : doThat();
|
||||
// </script>
|
||||
// Discussion:
|
||||
// The example above could produce `var x = 1/-2/i.test(s)...`
|
||||
// in which the first '/' is a mathematical division operator or it
|
||||
// could produce `/-2/i.test(s)` in which the first '/' starts a
|
||||
// regexp literal.
|
||||
// Look for missing semicolons inside branches, and maybe add
|
||||
// parentheses to make it clear which interpretation you intend.
|
||||
ErrSlashAmbig
|
||||
|
||||
// ErrPredefinedEscaper: "predefined escaper ... disallowed in template"
|
||||
// Example:
|
||||
// <div class={{. | html}}>Hello<div>
|
||||
// Discussion:
|
||||
// Package html/template already contextually escapes all pipelines to
|
||||
// produce HTML output safe against code injection. Manually escaping
|
||||
// pipeline output using the predefined escapers "html" or "urlquery" is
|
||||
// unnecessary, and may affect the correctness or safety of the escaped
|
||||
// pipeline output in Go 1.8 and earlier.
|
||||
//
|
||||
// In most cases, such as the given example, this error can be resolved by
|
||||
// simply removing the predefined escaper from the pipeline and letting the
|
||||
// contextual autoescaper handle the escaping of the pipeline. In other
|
||||
// instances, where the predefined escaper occurs in the middle of a
|
||||
// pipeline where subsequent commands expect escaped input, e.g.
|
||||
// {{.X | html | makeALink}}
|
||||
// where makeALink does
|
||||
// return `<a href="`+input+`">link</a>`
|
||||
// consider refactoring the surrounding template to make use of the
|
||||
// contextual autoescaper, i.e.
|
||||
// <a href="{{.X}}">link</a>
|
||||
//
|
||||
// To ease migration to Go 1.9 and beyond, "html" and "urlquery" will
|
||||
// continue to be allowed as the last command in a pipeline. However, if the
|
||||
// pipeline occurs in an unquoted attribute value context, "html" is
|
||||
// disallowed. Avoid using "html" and "urlquery" entirely in new templates.
|
||||
ErrPredefinedEscaper
|
||||
)
|
||||
|
||||
func (e *Error) Error() string {
|
||||
switch {
|
||||
case e.Node != nil:
|
||||
loc, _ := (*parse.Tree)(nil).ErrorContext(e.Node)
|
||||
return fmt.Sprintf("html/template:%s: %s", loc, e.Description)
|
||||
case e.Line != 0:
|
||||
return fmt.Sprintf("html/template:%s:%d: %s", e.Name, e.Line, e.Description)
|
||||
case e.Name != "":
|
||||
return fmt.Sprintf("html/template:%s: %s", e.Name, e.Description)
|
||||
}
|
||||
return "html/template: " + e.Description
|
||||
}
|
||||
|
||||
// errorf creates an error given a format string f and args.
|
||||
// The template Name still needs to be supplied.
|
||||
func errorf(k ErrorCode, node parse.Node, line int, f string, args ...interface{}) *Error {
|
||||
return &Error{k, node, "", line, fmt.Sprintf(f, args...)}
|
||||
}
|
891
tpl/internal/go_templates/htmltemplate/escape.go
Normal file
891
tpl/internal/go_templates/htmltemplate/escape.go
Normal file
|
@ -0,0 +1,891 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
// escapeTemplate rewrites the named template, which must be
|
||||
// associated with t, to guarantee that the output of any of the named
|
||||
// templates is properly escaped. If no error is returned, then the named templates have
|
||||
// been modified. Otherwise the named templates have been rendered
|
||||
// unusable.
|
||||
func escapeTemplate(tmpl *Template, node parse.Node, name string) error {
|
||||
c, _ := tmpl.esc.escapeTree(context{}, node, name, 0)
|
||||
var err error
|
||||
if c.err != nil {
|
||||
err, c.err.Name = c.err, name
|
||||
} else if c.state != stateText {
|
||||
err = &Error{ErrEndContext, nil, name, 0, fmt.Sprintf("ends in a non-text context: %v", c)}
|
||||
}
|
||||
if err != nil {
|
||||
// Prevent execution of unsafe templates.
|
||||
if t := tmpl.set[name]; t != nil {
|
||||
t.escapeErr = err
|
||||
t.text.Tree = nil
|
||||
t.Tree = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
tmpl.esc.commit()
|
||||
if t := tmpl.set[name]; t != nil {
|
||||
t.escapeErr = escapeOK
|
||||
t.Tree = t.text.Tree
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// evalArgs formats the list of arguments into a string. It is equivalent to
|
||||
// fmt.Sprint(args...), except that it deferences all pointers.
|
||||
func evalArgs(args ...interface{}) string {
|
||||
// Optimization for simple common case of a single string argument.
|
||||
if len(args) == 1 {
|
||||
if s, ok := args[0].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
for i, arg := range args {
|
||||
args[i] = indirectToStringerOrError(arg)
|
||||
}
|
||||
return fmt.Sprint(args...)
|
||||
}
|
||||
|
||||
// funcMap maps command names to functions that render their inputs safe.
|
||||
var funcMap = template.FuncMap{
|
||||
"_html_template_attrescaper": attrEscaper,
|
||||
"_html_template_commentescaper": commentEscaper,
|
||||
"_html_template_cssescaper": cssEscaper,
|
||||
"_html_template_cssvaluefilter": cssValueFilter,
|
||||
"_html_template_htmlnamefilter": htmlNameFilter,
|
||||
"_html_template_htmlescaper": htmlEscaper,
|
||||
"_html_template_jsregexpescaper": jsRegexpEscaper,
|
||||
"_html_template_jsstrescaper": jsStrEscaper,
|
||||
"_html_template_jsvalescaper": jsValEscaper,
|
||||
"_html_template_nospaceescaper": htmlNospaceEscaper,
|
||||
"_html_template_rcdataescaper": rcdataEscaper,
|
||||
"_html_template_srcsetescaper": srcsetFilterAndEscaper,
|
||||
"_html_template_urlescaper": urlEscaper,
|
||||
"_html_template_urlfilter": urlFilter,
|
||||
"_html_template_urlnormalizer": urlNormalizer,
|
||||
"_eval_args_": evalArgs,
|
||||
}
|
||||
|
||||
// escaper collects type inferences about templates and changes needed to make
|
||||
// templates injection safe.
|
||||
type escaper struct {
|
||||
// ns is the nameSpace that this escaper is associated with.
|
||||
ns *nameSpace
|
||||
// output[templateName] is the output context for a templateName that
|
||||
// has been mangled to include its input context.
|
||||
output map[string]context
|
||||
// derived[c.mangle(name)] maps to a template derived from the template
|
||||
// named name templateName for the start context c.
|
||||
derived map[string]*template.Template
|
||||
// called[templateName] is a set of called mangled template names.
|
||||
called map[string]bool
|
||||
// xxxNodeEdits are the accumulated edits to apply during commit.
|
||||
// Such edits are not applied immediately in case a template set
|
||||
// executes a given template in different escaping contexts.
|
||||
actionNodeEdits map[*parse.ActionNode][]string
|
||||
templateNodeEdits map[*parse.TemplateNode]string
|
||||
textNodeEdits map[*parse.TextNode][]byte
|
||||
}
|
||||
|
||||
// makeEscaper creates a blank escaper for the given set.
|
||||
func makeEscaper(n *nameSpace) escaper {
|
||||
return escaper{
|
||||
n,
|
||||
map[string]context{},
|
||||
map[string]*template.Template{},
|
||||
map[string]bool{},
|
||||
map[*parse.ActionNode][]string{},
|
||||
map[*parse.TemplateNode]string{},
|
||||
map[*parse.TextNode][]byte{},
|
||||
}
|
||||
}
|
||||
|
||||
// filterFailsafe is an innocuous word that is emitted in place of unsafe values
|
||||
// by sanitizer functions. It is not a keyword in any programming language,
|
||||
// contains no special characters, is not empty, and when it appears in output
|
||||
// it is distinct enough that a developer can find the source of the problem
|
||||
// via a search engine.
|
||||
const filterFailsafe = "ZgotmplZ"
|
||||
|
||||
// escape escapes a template node.
|
||||
func (e *escaper) escape(c context, n parse.Node) context {
|
||||
switch n := n.(type) {
|
||||
case *parse.ActionNode:
|
||||
return e.escapeAction(c, n)
|
||||
case *parse.IfNode:
|
||||
return e.escapeBranch(c, &n.BranchNode, "if")
|
||||
case *parse.ListNode:
|
||||
return e.escapeList(c, n)
|
||||
case *parse.RangeNode:
|
||||
return e.escapeBranch(c, &n.BranchNode, "range")
|
||||
case *parse.TemplateNode:
|
||||
return e.escapeTemplate(c, n)
|
||||
case *parse.TextNode:
|
||||
return e.escapeText(c, n)
|
||||
case *parse.WithNode:
|
||||
return e.escapeBranch(c, &n.BranchNode, "with")
|
||||
}
|
||||
panic("escaping " + n.String() + " is unimplemented")
|
||||
}
|
||||
|
||||
// escapeAction escapes an action template node.
|
||||
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
|
||||
if len(n.Pipe.Decl) != 0 {
|
||||
// A local variable assignment, not an interpolation.
|
||||
return c
|
||||
}
|
||||
c = nudge(c)
|
||||
// Check for disallowed use of predefined escapers in the pipeline.
|
||||
for pos, idNode := range n.Pipe.Cmds {
|
||||
node, ok := idNode.Args[0].(*parse.IdentifierNode)
|
||||
if !ok {
|
||||
// A predefined escaper "esc" will never be found as an identifier in a
|
||||
// Chain or Field node, since:
|
||||
// - "esc.x ..." is invalid, since predefined escapers return strings, and
|
||||
// strings do not have methods, keys or fields.
|
||||
// - "... .esc" is invalid, since predefined escapers are global functions,
|
||||
// not methods or fields of any types.
|
||||
// Therefore, it is safe to ignore these two node types.
|
||||
continue
|
||||
}
|
||||
ident := node.Ident
|
||||
if _, ok := predefinedEscapers[ident]; ok {
|
||||
if pos < len(n.Pipe.Cmds)-1 ||
|
||||
c.state == stateAttr && c.delim == delimSpaceOrTagEnd && ident == "html" {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s := make([]string, 0, 3)
|
||||
switch c.state {
|
||||
case stateError:
|
||||
return c
|
||||
case stateURL, stateCSSDqStr, stateCSSSqStr, stateCSSDqURL, stateCSSSqURL, stateCSSURL:
|
||||
switch c.urlPart {
|
||||
case urlPartNone:
|
||||
s = append(s, "_html_template_urlfilter")
|
||||
fallthrough
|
||||
case urlPartPreQuery:
|
||||
switch c.state {
|
||||
case stateCSSDqStr, stateCSSSqStr:
|
||||
s = append(s, "_html_template_cssescaper")
|
||||
default:
|
||||
s = append(s, "_html_template_urlnormalizer")
|
||||
}
|
||||
case urlPartQueryOrFrag:
|
||||
s = append(s, "_html_template_urlescaper")
|
||||
case urlPartUnknown:
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrAmbigContext, n, n.Line, "%s appears in an ambiguous context within a URL", n),
|
||||
}
|
||||
default:
|
||||
panic(c.urlPart.String())
|
||||
}
|
||||
case stateJS:
|
||||
s = append(s, "_html_template_jsvalescaper")
|
||||
// A slash after a value starts a div operator.
|
||||
c.jsCtx = jsCtxDivOp
|
||||
case stateJSDqStr, stateJSSqStr:
|
||||
s = append(s, "_html_template_jsstrescaper")
|
||||
case stateJSRegexp:
|
||||
s = append(s, "_html_template_jsregexpescaper")
|
||||
case stateCSS:
|
||||
s = append(s, "_html_template_cssvaluefilter")
|
||||
case stateText:
|
||||
s = append(s, "_html_template_htmlescaper")
|
||||
case stateRCDATA:
|
||||
s = append(s, "_html_template_rcdataescaper")
|
||||
case stateAttr:
|
||||
// Handled below in delim check.
|
||||
case stateAttrName, stateTag:
|
||||
c.state = stateAttrName
|
||||
s = append(s, "_html_template_htmlnamefilter")
|
||||
case stateSrcset:
|
||||
s = append(s, "_html_template_srcsetescaper")
|
||||
default:
|
||||
if isComment(c.state) {
|
||||
s = append(s, "_html_template_commentescaper")
|
||||
} else {
|
||||
panic("unexpected state " + c.state.String())
|
||||
}
|
||||
}
|
||||
switch c.delim {
|
||||
case delimNone:
|
||||
// No extra-escaping needed for raw text content.
|
||||
case delimSpaceOrTagEnd:
|
||||
s = append(s, "_html_template_nospaceescaper")
|
||||
default:
|
||||
s = append(s, "_html_template_attrescaper")
|
||||
}
|
||||
e.editActionNode(n, s)
|
||||
return c
|
||||
}
|
||||
|
||||
// ensurePipelineContains ensures that the pipeline ends with the commands with
|
||||
// the identifiers in s in order. If the pipeline ends with a predefined escaper
|
||||
// (i.e. "html" or "urlquery"), merge it with the identifiers in s.
|
||||
func ensurePipelineContains(p *parse.PipeNode, s []string) {
|
||||
if len(s) == 0 {
|
||||
// Do not rewrite pipeline if we have no escapers to insert.
|
||||
return
|
||||
}
|
||||
// Precondition: p.Cmds contains at most one predefined escaper and the
|
||||
// escaper will be present at p.Cmds[len(p.Cmds)-1]. This precondition is
|
||||
// always true because of the checks in escapeAction.
|
||||
pipelineLen := len(p.Cmds)
|
||||
if pipelineLen > 0 {
|
||||
lastCmd := p.Cmds[pipelineLen-1]
|
||||
if idNode, ok := lastCmd.Args[0].(*parse.IdentifierNode); ok {
|
||||
if esc := idNode.Ident; predefinedEscapers[esc] {
|
||||
// Pipeline ends with a predefined escaper.
|
||||
if len(p.Cmds) == 1 && len(lastCmd.Args) > 1 {
|
||||
// Special case: pipeline is of the form {{ esc arg1 arg2 ... argN }},
|
||||
// where esc is the predefined escaper, and arg1...argN are its arguments.
|
||||
// Convert this into the equivalent form
|
||||
// {{ _eval_args_ arg1 arg2 ... argN | esc }}, so that esc can be easily
|
||||
// merged with the escapers in s.
|
||||
lastCmd.Args[0] = parse.NewIdentifier("_eval_args_").SetTree(nil).SetPos(lastCmd.Args[0].Position())
|
||||
p.Cmds = appendCmd(p.Cmds, newIdentCmd(esc, p.Position()))
|
||||
pipelineLen++
|
||||
}
|
||||
// If any of the commands in s that we are about to insert is equivalent
|
||||
// to the predefined escaper, use the predefined escaper instead.
|
||||
dup := false
|
||||
for i, escaper := range s {
|
||||
if escFnsEq(esc, escaper) {
|
||||
s[i] = idNode.Ident
|
||||
dup = true
|
||||
}
|
||||
}
|
||||
if dup {
|
||||
// The predefined escaper will already be inserted along with the
|
||||
// escapers in s, so do not copy it to the rewritten pipeline.
|
||||
pipelineLen--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rewrite the pipeline, creating the escapers in s at the end of the pipeline.
|
||||
newCmds := make([]*parse.CommandNode, pipelineLen, pipelineLen+len(s))
|
||||
insertedIdents := make(map[string]bool)
|
||||
for i := 0; i < pipelineLen; i++ {
|
||||
cmd := p.Cmds[i]
|
||||
newCmds[i] = cmd
|
||||
if idNode, ok := cmd.Args[0].(*parse.IdentifierNode); ok {
|
||||
insertedIdents[normalizeEscFn(idNode.Ident)] = true
|
||||
}
|
||||
}
|
||||
for _, name := range s {
|
||||
if !insertedIdents[normalizeEscFn(name)] {
|
||||
// When two templates share an underlying parse tree via the use of
|
||||
// AddParseTree and one template is executed after the other, this check
|
||||
// ensures that escapers that were already inserted into the pipeline on
|
||||
// the first escaping pass do not get inserted again.
|
||||
newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position()))
|
||||
}
|
||||
}
|
||||
p.Cmds = newCmds
|
||||
}
|
||||
|
||||
// predefinedEscapers contains template predefined escapers that are equivalent
|
||||
// to some contextual escapers. Keep in sync with equivEscapers.
|
||||
var predefinedEscapers = map[string]bool{
|
||||
"html": true,
|
||||
"urlquery": true,
|
||||
}
|
||||
|
||||
// equivEscapers matches contextual escapers to equivalent predefined
|
||||
// template escapers.
|
||||
var equivEscapers = map[string]string{
|
||||
// The following pairs of HTML escapers provide equivalent security
|
||||
// guarantees, since they all escape '\000', '\'', '"', '&', '<', and '>'.
|
||||
"_html_template_attrescaper": "html",
|
||||
"_html_template_htmlescaper": "html",
|
||||
"_html_template_rcdataescaper": "html",
|
||||
// These two URL escapers produce URLs safe for embedding in a URL query by
|
||||
// percent-encoding all the reserved characters specified in RFC 3986 Section
|
||||
// 2.2
|
||||
"_html_template_urlescaper": "urlquery",
|
||||
// These two functions are not actually equivalent; urlquery is stricter as it
|
||||
// escapes reserved characters (e.g. '#'), while _html_template_urlnormalizer
|
||||
// does not. It is therefore only safe to replace _html_template_urlnormalizer
|
||||
// with urlquery (this happens in ensurePipelineContains), but not the otherI've
|
||||
// way around. We keep this entry around to preserve the behavior of templates
|
||||
// written before Go 1.9, which might depend on this substitution taking place.
|
||||
"_html_template_urlnormalizer": "urlquery",
|
||||
}
|
||||
|
||||
// escFnsEq reports whether the two escaping functions are equivalent.
|
||||
func escFnsEq(a, b string) bool {
|
||||
return normalizeEscFn(a) == normalizeEscFn(b)
|
||||
}
|
||||
|
||||
// normalizeEscFn(a) is equal to normalizeEscFn(b) for any pair of names of
|
||||
// escaper functions a and b that are equivalent.
|
||||
func normalizeEscFn(e string) string {
|
||||
if norm := equivEscapers[e]; norm != "" {
|
||||
return norm
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x)
|
||||
// for all x.
|
||||
var redundantFuncs = map[string]map[string]bool{
|
||||
"_html_template_commentescaper": {
|
||||
"_html_template_attrescaper": true,
|
||||
"_html_template_nospaceescaper": true,
|
||||
"_html_template_htmlescaper": true,
|
||||
},
|
||||
"_html_template_cssescaper": {
|
||||
"_html_template_attrescaper": true,
|
||||
},
|
||||
"_html_template_jsregexpescaper": {
|
||||
"_html_template_attrescaper": true,
|
||||
},
|
||||
"_html_template_jsstrescaper": {
|
||||
"_html_template_attrescaper": true,
|
||||
},
|
||||
"_html_template_urlescaper": {
|
||||
"_html_template_urlnormalizer": true,
|
||||
},
|
||||
}
|
||||
|
||||
// appendCmd appends the given command to the end of the command pipeline
|
||||
// unless it is redundant with the last command.
|
||||
func appendCmd(cmds []*parse.CommandNode, cmd *parse.CommandNode) []*parse.CommandNode {
|
||||
if n := len(cmds); n != 0 {
|
||||
last, okLast := cmds[n-1].Args[0].(*parse.IdentifierNode)
|
||||
next, okNext := cmd.Args[0].(*parse.IdentifierNode)
|
||||
if okLast && okNext && redundantFuncs[last.Ident][next.Ident] {
|
||||
return cmds
|
||||
}
|
||||
}
|
||||
return append(cmds, cmd)
|
||||
}
|
||||
|
||||
// newIdentCmd produces a command containing a single identifier node.
|
||||
func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode {
|
||||
return &parse.CommandNode{
|
||||
NodeType: parse.NodeCommand,
|
||||
Args: []parse.Node{parse.NewIdentifier(identifier).SetTree(nil).SetPos(pos)}, // TODO: SetTree.
|
||||
}
|
||||
}
|
||||
|
||||
// nudge returns the context that would result from following empty string
|
||||
// transitions from the input context.
|
||||
// For example, parsing:
|
||||
// `<a href=`
|
||||
// will end in context{stateBeforeValue, attrURL}, but parsing one extra rune:
|
||||
// `<a href=x`
|
||||
// will end in context{stateURL, delimSpaceOrTagEnd, ...}.
|
||||
// There are two transitions that happen when the 'x' is seen:
|
||||
// (1) Transition from a before-value state to a start-of-value state without
|
||||
// consuming any character.
|
||||
// (2) Consume 'x' and transition past the first value character.
|
||||
// In this case, nudging produces the context after (1) happens.
|
||||
func nudge(c context) context {
|
||||
switch c.state {
|
||||
case stateTag:
|
||||
// In `<foo {{.}}`, the action should emit an attribute.
|
||||
c.state = stateAttrName
|
||||
case stateBeforeValue:
|
||||
// In `<foo bar={{.}}`, the action is an undelimited value.
|
||||
c.state, c.delim, c.attr = attrStartStates[c.attr], delimSpaceOrTagEnd, attrNone
|
||||
case stateAfterName:
|
||||
// In `<foo bar {{.}}`, the action is an attribute name.
|
||||
c.state, c.attr = stateAttrName, attrNone
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// join joins the two contexts of a branch template node. The result is an
|
||||
// error context if either of the input contexts are error contexts, or if the
|
||||
// input contexts differ.
|
||||
func join(a, b context, node parse.Node, nodeName string) context {
|
||||
if a.state == stateError {
|
||||
return a
|
||||
}
|
||||
if b.state == stateError {
|
||||
return b
|
||||
}
|
||||
if a.eq(b) {
|
||||
return a
|
||||
}
|
||||
|
||||
c := a
|
||||
c.urlPart = b.urlPart
|
||||
if c.eq(b) {
|
||||
// The contexts differ only by urlPart.
|
||||
c.urlPart = urlPartUnknown
|
||||
return c
|
||||
}
|
||||
|
||||
c = a
|
||||
c.jsCtx = b.jsCtx
|
||||
if c.eq(b) {
|
||||
// The contexts differ only by jsCtx.
|
||||
c.jsCtx = jsCtxUnknown
|
||||
return c
|
||||
}
|
||||
|
||||
// Allow a nudged context to join with an unnudged one.
|
||||
// This means that
|
||||
// <p title={{if .C}}{{.}}{{end}}
|
||||
// ends in an unquoted value state even though the else branch
|
||||
// ends in stateBeforeValue.
|
||||
if c, d := nudge(a), nudge(b); !(c.eq(a) && d.eq(b)) {
|
||||
if e := join(c, d, node, nodeName); e.state != stateError {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrBranchEnd, node, 0, "{{%s}} branches end in different contexts: %v, %v", nodeName, a, b),
|
||||
}
|
||||
}
|
||||
|
||||
// escapeBranch escapes a branch template node: "if", "range" and "with".
|
||||
func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context {
|
||||
c0 := e.escapeList(c, n.List)
|
||||
if nodeName == "range" && c0.state != stateError {
|
||||
// The "true" branch of a "range" node can execute multiple times.
|
||||
// We check that executing n.List once results in the same context
|
||||
// as executing n.List twice.
|
||||
c1, _ := e.escapeListConditionally(c0, n.List, nil)
|
||||
c0 = join(c0, c1, n, nodeName)
|
||||
if c0.state == stateError {
|
||||
// Make clear that this is a problem on loop re-entry
|
||||
// since developers tend to overlook that branch when
|
||||
// debugging templates.
|
||||
c0.err.Line = n.Line
|
||||
c0.err.Description = "on range loop re-entry: " + c0.err.Description
|
||||
return c0
|
||||
}
|
||||
}
|
||||
c1 := e.escapeList(c, n.ElseList)
|
||||
return join(c0, c1, n, nodeName)
|
||||
}
|
||||
|
||||
// escapeList escapes a list template node.
|
||||
func (e *escaper) escapeList(c context, n *parse.ListNode) context {
|
||||
if n == nil {
|
||||
return c
|
||||
}
|
||||
for _, m := range n.Nodes {
|
||||
c = e.escape(c, m)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// escapeListConditionally escapes a list node but only preserves edits and
|
||||
// inferences in e if the inferences and output context satisfy filter.
|
||||
// It returns the best guess at an output context, and the result of the filter
|
||||
// which is the same as whether e was updated.
|
||||
func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) {
|
||||
e1 := makeEscaper(e.ns)
|
||||
// Make type inferences available to f.
|
||||
for k, v := range e.output {
|
||||
e1.output[k] = v
|
||||
}
|
||||
c = e1.escapeList(c, n)
|
||||
ok := filter != nil && filter(&e1, c)
|
||||
if ok {
|
||||
// Copy inferences and edits from e1 back into e.
|
||||
for k, v := range e1.output {
|
||||
e.output[k] = v
|
||||
}
|
||||
for k, v := range e1.derived {
|
||||
e.derived[k] = v
|
||||
}
|
||||
for k, v := range e1.called {
|
||||
e.called[k] = v
|
||||
}
|
||||
for k, v := range e1.actionNodeEdits {
|
||||
e.editActionNode(k, v)
|
||||
}
|
||||
for k, v := range e1.templateNodeEdits {
|
||||
e.editTemplateNode(k, v)
|
||||
}
|
||||
for k, v := range e1.textNodeEdits {
|
||||
e.editTextNode(k, v)
|
||||
}
|
||||
}
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// escapeTemplate escapes a {{template}} call node.
|
||||
func (e *escaper) escapeTemplate(c context, n *parse.TemplateNode) context {
|
||||
c, name := e.escapeTree(c, n, n.Name, n.Line)
|
||||
if name != n.Name {
|
||||
e.editTemplateNode(n, name)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// escapeTree escapes the named template starting in the given context as
|
||||
// necessary and returns its output context.
|
||||
func (e *escaper) escapeTree(c context, node parse.Node, name string, line int) (context, string) {
|
||||
// Mangle the template name with the input context to produce a reliable
|
||||
// identifier.
|
||||
dname := c.mangle(name)
|
||||
e.called[dname] = true
|
||||
if out, ok := e.output[dname]; ok {
|
||||
// Already escaped.
|
||||
return out, dname
|
||||
}
|
||||
t := e.template(name)
|
||||
if t == nil {
|
||||
// Two cases: The template exists but is empty, or has never been mentioned at
|
||||
// all. Distinguish the cases in the error messages.
|
||||
if e.ns.set[name] != nil {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrNoSuchTemplate, node, line, "%q is an incomplete or empty template", name),
|
||||
}, dname
|
||||
}
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrNoSuchTemplate, node, line, "no such template %q", name),
|
||||
}, dname
|
||||
}
|
||||
if dname != name {
|
||||
// Use any template derived during an earlier call to escapeTemplate
|
||||
// with different top level templates, or clone if necessary.
|
||||
dt := e.template(dname)
|
||||
if dt == nil {
|
||||
dt = template.New(dname)
|
||||
dt.Tree = &parse.Tree{Name: dname, Root: t.Root.CopyList()}
|
||||
e.derived[dname] = dt
|
||||
}
|
||||
t = dt
|
||||
}
|
||||
return e.computeOutCtx(c, t), dname
|
||||
}
|
||||
|
||||
// computeOutCtx takes a template and its start context and computes the output
|
||||
// context while storing any inferences in e.
|
||||
func (e *escaper) computeOutCtx(c context, t *template.Template) context {
|
||||
// Propagate context over the body.
|
||||
c1, ok := e.escapeTemplateBody(c, t)
|
||||
if !ok {
|
||||
// Look for a fixed point by assuming c1 as the output context.
|
||||
if c2, ok2 := e.escapeTemplateBody(c1, t); ok2 {
|
||||
c1, ok = c2, true
|
||||
}
|
||||
// Use c1 as the error context if neither assumption worked.
|
||||
}
|
||||
if !ok && c1.state != stateError {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrOutputContext, t.Tree.Root, 0, "cannot compute output context for template %s", t.Name()),
|
||||
}
|
||||
}
|
||||
return c1
|
||||
}
|
||||
|
||||
// escapeTemplateBody escapes the given template assuming the given output
|
||||
// context, and returns the best guess at the output context and whether the
|
||||
// assumption was correct.
|
||||
func (e *escaper) escapeTemplateBody(c context, t *template.Template) (context, bool) {
|
||||
filter := func(e1 *escaper, c1 context) bool {
|
||||
if c1.state == stateError {
|
||||
// Do not update the input escaper, e.
|
||||
return false
|
||||
}
|
||||
if !e1.called[t.Name()] {
|
||||
// If t is not recursively called, then c1 is an
|
||||
// accurate output context.
|
||||
return true
|
||||
}
|
||||
// c1 is accurate if it matches our assumed output context.
|
||||
return c.eq(c1)
|
||||
}
|
||||
// We need to assume an output context so that recursive template calls
|
||||
// take the fast path out of escapeTree instead of infinitely recursing.
|
||||
// Naively assuming that the input context is the same as the output
|
||||
// works >90% of the time.
|
||||
e.output[t.Name()] = c
|
||||
return e.escapeListConditionally(c, t.Tree.Root, filter)
|
||||
}
|
||||
|
||||
// delimEnds maps each delim to a string of characters that terminate it.
|
||||
var delimEnds = [...]string{
|
||||
delimDoubleQuote: `"`,
|
||||
delimSingleQuote: "'",
|
||||
// Determined empirically by running the below in various browsers.
|
||||
// var div = document.createElement("DIV");
|
||||
// for (var i = 0; i < 0x10000; ++i) {
|
||||
// div.innerHTML = "<span title=x" + String.fromCharCode(i) + "-bar>";
|
||||
// if (div.getElementsByTagName("SPAN")[0].title.indexOf("bar") < 0)
|
||||
// document.write("<p>U+" + i.toString(16));
|
||||
// }
|
||||
delimSpaceOrTagEnd: " \t\n\f\r>",
|
||||
}
|
||||
|
||||
var doctypeBytes = []byte("<!DOCTYPE")
|
||||
|
||||
// escapeText escapes a text template node.
|
||||
func (e *escaper) escapeText(c context, n *parse.TextNode) context {
|
||||
s, written, i, b := n.Text, 0, 0, new(bytes.Buffer)
|
||||
for i != len(s) {
|
||||
c1, nread := contextAfterText(c, s[i:])
|
||||
i1 := i + nread
|
||||
if c.state == stateText || c.state == stateRCDATA {
|
||||
end := i1
|
||||
if c1.state != c.state {
|
||||
for j := end - 1; j >= i; j-- {
|
||||
if s[j] == '<' {
|
||||
end = j
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for j := i; j < end; j++ {
|
||||
if s[j] == '<' && !bytes.HasPrefix(bytes.ToUpper(s[j:]), doctypeBytes) {
|
||||
b.Write(s[written:j])
|
||||
b.WriteString("<")
|
||||
written = j + 1
|
||||
}
|
||||
}
|
||||
} else if isComment(c.state) && c.delim == delimNone {
|
||||
switch c.state {
|
||||
case stateJSBlockCmt:
|
||||
// https://es5.github.com/#x7.4:
|
||||
// "Comments behave like white space and are
|
||||
// discarded except that, if a MultiLineComment
|
||||
// contains a line terminator character, then
|
||||
// the entire comment is considered to be a
|
||||
// LineTerminator for purposes of parsing by
|
||||
// the syntactic grammar."
|
||||
if bytes.ContainsAny(s[written:i1], "\n\r\u2028\u2029") {
|
||||
b.WriteByte('\n')
|
||||
} else {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
case stateCSSBlockCmt:
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
written = i1
|
||||
}
|
||||
if c.state != c1.state && isComment(c1.state) && c1.delim == delimNone {
|
||||
// Preserve the portion between written and the comment start.
|
||||
cs := i1 - 2
|
||||
if c1.state == stateHTMLCmt {
|
||||
// "<!--" instead of "/*" or "//"
|
||||
cs -= 2
|
||||
}
|
||||
b.Write(s[written:cs])
|
||||
written = i1
|
||||
}
|
||||
if i == i1 && c.state == c1.state {
|
||||
panic(fmt.Sprintf("infinite loop from %v to %v on %q..%q", c, c1, s[:i], s[i:]))
|
||||
}
|
||||
c, i = c1, i1
|
||||
}
|
||||
|
||||
if written != 0 && c.state != stateError {
|
||||
if !isComment(c.state) || c.delim != delimNone {
|
||||
b.Write(n.Text[written:])
|
||||
}
|
||||
e.editTextNode(n, b.Bytes())
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// contextAfterText starts in context c, consumes some tokens from the front of
|
||||
// s, then returns the context after those tokens and the unprocessed suffix.
|
||||
func contextAfterText(c context, s []byte) (context, int) {
|
||||
if c.delim == delimNone {
|
||||
c1, i := tSpecialTagEnd(c, s)
|
||||
if i == 0 {
|
||||
// A special end tag (`</script>`) has been seen and
|
||||
// all content preceding it has been consumed.
|
||||
return c1, 0
|
||||
}
|
||||
// Consider all content up to any end tag.
|
||||
return transitionFunc[c.state](c, s[:i])
|
||||
}
|
||||
|
||||
// We are at the beginning of an attribute value.
|
||||
|
||||
i := bytes.IndexAny(s, delimEnds[c.delim])
|
||||
if i == -1 {
|
||||
i = len(s)
|
||||
}
|
||||
if c.delim == delimSpaceOrTagEnd {
|
||||
// https://www.w3.org/TR/html5/syntax.html#attribute-value-(unquoted)-state
|
||||
// lists the runes below as error characters.
|
||||
// Error out because HTML parsers may differ on whether
|
||||
// "<a id= onclick=f(" ends inside id's or onclick's value,
|
||||
// "<a class=`foo " ends inside a value,
|
||||
// "<a style=font:'Arial'" needs open-quote fixup.
|
||||
// IE treats '`' as a quotation character.
|
||||
if j := bytes.IndexAny(s[:i], "\"'<=`"); j >= 0 {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrBadHTML, nil, 0, "%q in unquoted attr: %q", s[j:j+1], s[:i]),
|
||||
}, len(s)
|
||||
}
|
||||
}
|
||||
if i == len(s) {
|
||||
// Remain inside the attribute.
|
||||
// Decode the value so non-HTML rules can easily handle
|
||||
// <button onclick="alert("Hi!")">
|
||||
// without having to entity decode token boundaries.
|
||||
for u := []byte(html.UnescapeString(string(s))); len(u) != 0; {
|
||||
c1, i1 := transitionFunc[c.state](c, u)
|
||||
c, u = c1, u[i1:]
|
||||
}
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
element := c.element
|
||||
|
||||
// If this is a non-JS "type" attribute inside "script" tag, do not treat the contents as JS.
|
||||
if c.state == stateAttr && c.element == elementScript && c.attr == attrScriptType && !isJSType(string(s[:i])) {
|
||||
element = elementNone
|
||||
}
|
||||
|
||||
if c.delim != delimSpaceOrTagEnd {
|
||||
// Consume any quote.
|
||||
i++
|
||||
}
|
||||
// On exiting an attribute, we discard all state information
|
||||
// except the state and element.
|
||||
return context{state: stateTag, element: element}, i
|
||||
}
|
||||
|
||||
// editActionNode records a change to an action pipeline for later commit.
|
||||
func (e *escaper) editActionNode(n *parse.ActionNode, cmds []string) {
|
||||
if _, ok := e.actionNodeEdits[n]; ok {
|
||||
panic(fmt.Sprintf("node %s shared between templates", n))
|
||||
}
|
||||
e.actionNodeEdits[n] = cmds
|
||||
}
|
||||
|
||||
// editTemplateNode records a change to a {{template}} callee for later commit.
|
||||
func (e *escaper) editTemplateNode(n *parse.TemplateNode, callee string) {
|
||||
if _, ok := e.templateNodeEdits[n]; ok {
|
||||
panic(fmt.Sprintf("node %s shared between templates", n))
|
||||
}
|
||||
e.templateNodeEdits[n] = callee
|
||||
}
|
||||
|
||||
// editTextNode records a change to a text node for later commit.
|
||||
func (e *escaper) editTextNode(n *parse.TextNode, text []byte) {
|
||||
if _, ok := e.textNodeEdits[n]; ok {
|
||||
panic(fmt.Sprintf("node %s shared between templates", n))
|
||||
}
|
||||
e.textNodeEdits[n] = text
|
||||
}
|
||||
|
||||
// commit applies changes to actions and template calls needed to contextually
|
||||
// autoescape content and adds any derived templates to the set.
|
||||
func (e *escaper) commit() {
|
||||
for name := range e.output {
|
||||
e.template(name).Funcs(funcMap)
|
||||
}
|
||||
// Any template from the name space associated with this escaper can be used
|
||||
// to add derived templates to the underlying text/template name space.
|
||||
tmpl := e.arbitraryTemplate()
|
||||
for _, t := range e.derived {
|
||||
if _, err := tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil {
|
||||
panic("error adding derived template")
|
||||
}
|
||||
}
|
||||
for n, s := range e.actionNodeEdits {
|
||||
ensurePipelineContains(n.Pipe, s)
|
||||
}
|
||||
for n, name := range e.templateNodeEdits {
|
||||
n.Name = name
|
||||
}
|
||||
for n, s := range e.textNodeEdits {
|
||||
n.Text = s
|
||||
}
|
||||
// Reset state that is specific to this commit so that the same changes are
|
||||
// not re-applied to the template on subsequent calls to commit.
|
||||
e.called = make(map[string]bool)
|
||||
e.actionNodeEdits = make(map[*parse.ActionNode][]string)
|
||||
e.templateNodeEdits = make(map[*parse.TemplateNode]string)
|
||||
e.textNodeEdits = make(map[*parse.TextNode][]byte)
|
||||
}
|
||||
|
||||
// template returns the named template given a mangled template name.
|
||||
func (e *escaper) template(name string) *template.Template {
|
||||
// Any template from the name space associated with this escaper can be used
|
||||
// to look up templates in the underlying text/template name space.
|
||||
t := e.arbitraryTemplate().text.Lookup(name)
|
||||
if t == nil {
|
||||
t = e.derived[name]
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// arbitraryTemplate returns an arbitrary template from the name space
|
||||
// associated with e and panics if no templates are found.
|
||||
func (e *escaper) arbitraryTemplate() *Template {
|
||||
for _, t := range e.ns.set {
|
||||
return t
|
||||
}
|
||||
panic("no templates in name space")
|
||||
}
|
||||
|
||||
// Forwarding functions so that clients need only import this package
|
||||
// to reach the general escaping functions of text/template.
|
||||
|
||||
// HTMLEscape writes to w the escaped HTML equivalent of the plain text data b.
|
||||
func HTMLEscape(w io.Writer, b []byte) {
|
||||
template.HTMLEscape(w, b)
|
||||
}
|
||||
|
||||
// HTMLEscapeString returns the escaped HTML equivalent of the plain text data s.
|
||||
func HTMLEscapeString(s string) string {
|
||||
return template.HTMLEscapeString(s)
|
||||
}
|
||||
|
||||
// HTMLEscaper returns the escaped HTML equivalent of the textual
|
||||
// representation of its arguments.
|
||||
func HTMLEscaper(args ...interface{}) string {
|
||||
return template.HTMLEscaper(args...)
|
||||
}
|
||||
|
||||
// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
|
||||
func JSEscape(w io.Writer, b []byte) {
|
||||
template.JSEscape(w, b)
|
||||
}
|
||||
|
||||
// JSEscapeString returns the escaped JavaScript equivalent of the plain text data s.
|
||||
func JSEscapeString(s string) string {
|
||||
return template.JSEscapeString(s)
|
||||
}
|
||||
|
||||
// JSEscaper returns the escaped JavaScript equivalent of the textual
|
||||
// representation of its arguments.
|
||||
func JSEscaper(args ...interface{}) string {
|
||||
return template.JSEscaper(args...)
|
||||
}
|
||||
|
||||
// URLQueryEscaper returns the escaped value of the textual representation of
|
||||
// its arguments in a form suitable for embedding in a URL query.
|
||||
func URLQueryEscaper(args ...interface{}) string {
|
||||
return template.URLQueryEscaper(args...)
|
||||
}
|
1973
tpl/internal/go_templates/htmltemplate/escape_test.go
Normal file
1973
tpl/internal/go_templates/htmltemplate/escape_test.go
Normal file
File diff suppressed because it is too large
Load diff
184
tpl/internal/go_templates/htmltemplate/example_test.go
Normal file
184
tpl/internal/go_templates/htmltemplate/example_test.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
const tpl = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
check := func(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
t, err := template.New("webpage").Parse(tpl)
|
||||
check(err)
|
||||
|
||||
data := struct {
|
||||
Title string
|
||||
Items []string
|
||||
}{
|
||||
Title: "My page",
|
||||
Items: []string{
|
||||
"My photos",
|
||||
"My blog",
|
||||
},
|
||||
}
|
||||
|
||||
err = t.Execute(os.Stdout, data)
|
||||
check(err)
|
||||
|
||||
noItems := struct {
|
||||
Title string
|
||||
Items []string
|
||||
}{
|
||||
Title: "My another page",
|
||||
Items: []string{},
|
||||
}
|
||||
|
||||
err = t.Execute(os.Stdout, noItems)
|
||||
check(err)
|
||||
|
||||
// Output:
|
||||
// <!DOCTYPE html>
|
||||
// <html>
|
||||
// <head>
|
||||
// <meta charset="UTF-8">
|
||||
// <title>My page</title>
|
||||
// </head>
|
||||
// <body>
|
||||
// <div>My photos</div><div>My blog</div>
|
||||
// </body>
|
||||
// </html>
|
||||
// <!DOCTYPE html>
|
||||
// <html>
|
||||
// <head>
|
||||
// <meta charset="UTF-8">
|
||||
// <title>My another page</title>
|
||||
// </head>
|
||||
// <body>
|
||||
// <div><strong>no rows</strong></div>
|
||||
// </body>
|
||||
// </html>
|
||||
|
||||
}
|
||||
|
||||
func Example_autoescaping() {
|
||||
check := func(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
check(err)
|
||||
err = t.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have been pwned')</script>")
|
||||
check(err)
|
||||
// Output:
|
||||
// Hello, <script>alert('you have been pwned')</script>!
|
||||
}
|
||||
|
||||
func Example_escape() {
|
||||
const s = `"Fran & Freddie's Diner" <tasty@example.com>`
|
||||
v := []interface{}{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`}
|
||||
|
||||
fmt.Println(template.HTMLEscapeString(s))
|
||||
template.HTMLEscape(os.Stdout, []byte(s))
|
||||
fmt.Fprintln(os.Stdout, "")
|
||||
fmt.Println(template.HTMLEscaper(v...))
|
||||
|
||||
fmt.Println(template.JSEscapeString(s))
|
||||
template.JSEscape(os.Stdout, []byte(s))
|
||||
fmt.Fprintln(os.Stdout, "")
|
||||
fmt.Println(template.JSEscaper(v...))
|
||||
|
||||
fmt.Println(template.URLQueryEscaper(v...))
|
||||
|
||||
// Output:
|
||||
// "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\" \x3Ctasty@example.com\x3E
|
||||
// \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
|
||||
// \"Fran & Freddie\'s Diner\"32\x3Ctasty@example.com\x3E
|
||||
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
|
||||
|
||||
}
|
||||
|
||||
func ExampleTemplate_Delims() {
|
||||
const text = "<<.Greeting>> {{.Name}}"
|
||||
|
||||
data := struct {
|
||||
Greeting string
|
||||
Name string
|
||||
}{
|
||||
Greeting: "Hello",
|
||||
Name: "Joe",
|
||||
}
|
||||
|
||||
t := template.Must(template.New("tpl").Delims("<<", ">>").Parse(text))
|
||||
|
||||
err := t.Execute(os.Stdout, data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Hello {{.Name}}
|
||||
}
|
||||
|
||||
// The following example is duplicated in text/template; keep them in sync.
|
||||
|
||||
func ExampleTemplate_block() {
|
||||
const (
|
||||
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
|
||||
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
|
||||
)
|
||||
var (
|
||||
funcs = template.FuncMap{"join": strings.Join}
|
||||
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
|
||||
)
|
||||
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// Names:
|
||||
// - Gamora
|
||||
// - Groot
|
||||
// - Nebula
|
||||
// - Rocket
|
||||
// - Star-Lord
|
||||
// Names: Gamora, Groot, Nebula, Rocket, Star-Lord
|
||||
}
|
229
tpl/internal/go_templates/htmltemplate/examplefiles_test.go
Normal file
229
tpl/internal/go_templates/htmltemplate/examplefiles_test.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
// 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 go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
)
|
||||
|
||||
// templateFile defines the contents of a template to be stored in a file, for testing.
|
||||
type templateFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func createTestDir(files []templateFile) string {
|
||||
dir, err := ioutil.TempDir("", "template")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
f, err := os.Create(filepath.Join(dir, file.name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.WriteString(f, file.contents)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// The following example is duplicated in text/template; keep them in sync.
|
||||
|
||||
// Here we demonstrate loading a set of templates from a directory.
|
||||
func ExampleTemplate_glob() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// T0.tmpl is the first name matched, so it becomes the starting template,
|
||||
// the value returned by ParseGlob.
|
||||
tmpl := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
err := tmpl.Execute(os.Stdout, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("template execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// T0 invokes T1: (T1 invokes T2: (This is T2))
|
||||
}
|
||||
|
||||
// Here we demonstrate loading a set of templates from files in different directories
|
||||
func ExampleTemplate_parsefiles() {
|
||||
// Here we create different temporary directories and populate them with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir1 := createTestDir([]templateFile{
|
||||
// T1.tmpl is a plain template file that just invokes T2.
|
||||
{"T1.tmpl", `T1 invokes T2: ({{template "T2"}})`},
|
||||
})
|
||||
|
||||
dir2 := createTestDir([]templateFile{
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer func(dirs ...string) {
|
||||
for _, dir := range dirs {
|
||||
os.RemoveAll(dir)
|
||||
}
|
||||
}(dir1, dir2)
|
||||
|
||||
// Here starts the example proper.
|
||||
// Let's just parse only dir1/T0 and dir2/T2
|
||||
paths := []string{
|
||||
filepath.Join(dir1, "T1.tmpl"),
|
||||
filepath.Join(dir2, "T2.tmpl"),
|
||||
}
|
||||
tmpl := template.Must(template.ParseFiles(paths...))
|
||||
|
||||
err := tmpl.Execute(os.Stdout, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("template execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// T1 invokes T2: (This is T2)
|
||||
}
|
||||
|
||||
// The following example is duplicated in text/template; keep them in sync.
|
||||
|
||||
// This example demonstrates one way to share some templates
|
||||
// and use them in different contexts. In this variant we add multiple driver
|
||||
// templates by hand to an existing bundle of templates.
|
||||
func ExampleTemplate_helpers() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the helpers.
|
||||
templates := template.Must(template.ParseGlob(pattern))
|
||||
// Add one driver template to the bunch; we do this with an explicit template definition.
|
||||
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver1: ", err)
|
||||
}
|
||||
// Add another driver template.
|
||||
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver2: ", err)
|
||||
}
|
||||
// We load all the templates before execution. This package does not require
|
||||
// that behavior but html/template's escaping does, so it's a good habit.
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver1 execution: %s", err)
|
||||
}
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver2 execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// Driver 1 calls T1: (T1 invokes T2: (This is T2))
|
||||
// Driver 2 calls T2: (This is T2)
|
||||
}
|
||||
|
||||
// The following example is duplicated in text/template; keep them in sync.
|
||||
|
||||
// This example demonstrates how to use one group of driver
|
||||
// templates with distinct sets of helper templates.
|
||||
func ExampleTemplate_share() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
|
||||
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the drivers.
|
||||
drivers := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
// We must define an implementation of the T2 template. First we clone
|
||||
// the drivers, then add a definition of T2 to the template name space.
|
||||
|
||||
// 1. Clone the helper set to create a new name space from which to run them.
|
||||
first, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning helpers: ", err)
|
||||
}
|
||||
// 2. Define T2, version A, and parse it.
|
||||
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Now repeat the whole thing, using a different version of T2.
|
||||
// 1. Clone the drivers.
|
||||
second, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning drivers: ", err)
|
||||
}
|
||||
// 2. Define T2, version B, and parse it.
|
||||
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Execute the templates in the reverse order to verify the
|
||||
// first is unaffected by the second.
|
||||
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
|
||||
if err != nil {
|
||||
log.Fatalf("second execution: %s", err)
|
||||
}
|
||||
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
|
||||
if err != nil {
|
||||
log.Fatalf("first: execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
|
||||
// T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
|
||||
}
|
266
tpl/internal/go_templates/htmltemplate/html.go
Normal file
266
tpl/internal/go_templates/htmltemplate/html.go
Normal file
|
@ -0,0 +1,266 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// htmlNospaceEscaper escapes for inclusion in unquoted attribute values.
|
||||
func htmlNospaceEscaper(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeHTML {
|
||||
return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false)
|
||||
}
|
||||
return htmlReplacer(s, htmlNospaceReplacementTable, false)
|
||||
}
|
||||
|
||||
// attrEscaper escapes for inclusion in quoted attribute values.
|
||||
func attrEscaper(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeHTML {
|
||||
return htmlReplacer(stripTags(s), htmlNormReplacementTable, true)
|
||||
}
|
||||
return htmlReplacer(s, htmlReplacementTable, true)
|
||||
}
|
||||
|
||||
// rcdataEscaper escapes for inclusion in an RCDATA element body.
|
||||
func rcdataEscaper(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeHTML {
|
||||
return htmlReplacer(s, htmlNormReplacementTable, true)
|
||||
}
|
||||
return htmlReplacer(s, htmlReplacementTable, true)
|
||||
}
|
||||
|
||||
// htmlEscaper escapes for inclusion in HTML text.
|
||||
func htmlEscaper(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeHTML {
|
||||
return s
|
||||
}
|
||||
return htmlReplacer(s, htmlReplacementTable, true)
|
||||
}
|
||||
|
||||
// htmlReplacementTable contains the runes that need to be escaped
|
||||
// inside a quoted attribute value or in a text node.
|
||||
var htmlReplacementTable = []string{
|
||||
// https://www.w3.org/TR/html5/syntax.html#attribute-value-(unquoted)-state
|
||||
// U+0000 NULL Parse error. Append a U+FFFD REPLACEMENT
|
||||
// CHARACTER character to the current attribute's value.
|
||||
// "
|
||||
// and similarly
|
||||
// https://www.w3.org/TR/html5/syntax.html#before-attribute-value-state
|
||||
0: "\uFFFD",
|
||||
'"': """,
|
||||
'&': "&",
|
||||
'\'': "'",
|
||||
'+': "+",
|
||||
'<': "<",
|
||||
'>': ">",
|
||||
}
|
||||
|
||||
// htmlNormReplacementTable is like htmlReplacementTable but without '&' to
|
||||
// avoid over-encoding existing entities.
|
||||
var htmlNormReplacementTable = []string{
|
||||
0: "\uFFFD",
|
||||
'"': """,
|
||||
'\'': "'",
|
||||
'+': "+",
|
||||
'<': "<",
|
||||
'>': ">",
|
||||
}
|
||||
|
||||
// htmlNospaceReplacementTable contains the runes that need to be escaped
|
||||
// inside an unquoted attribute value.
|
||||
// The set of runes escaped is the union of the HTML specials and
|
||||
// those determined by running the JS below in browsers:
|
||||
// <div id=d></div>
|
||||
// <script>(function () {
|
||||
// var a = [], d = document.getElementById("d"), i, c, s;
|
||||
// for (i = 0; i < 0x10000; ++i) {
|
||||
// c = String.fromCharCode(i);
|
||||
// d.innerHTML = "<span title=" + c + "lt" + c + "></span>"
|
||||
// s = d.getElementsByTagName("SPAN")[0];
|
||||
// if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); }
|
||||
// }
|
||||
// document.write(a.join(", "));
|
||||
// })()</script>
|
||||
var htmlNospaceReplacementTable = []string{
|
||||
0: "�",
|
||||
'\t': "	",
|
||||
'\n': " ",
|
||||
'\v': "",
|
||||
'\f': "",
|
||||
'\r': " ",
|
||||
' ': " ",
|
||||
'"': """,
|
||||
'&': "&",
|
||||
'\'': "'",
|
||||
'+': "+",
|
||||
'<': "<",
|
||||
'=': "=",
|
||||
'>': ">",
|
||||
// A parse error in the attribute value (unquoted) and
|
||||
// before attribute value states.
|
||||
// Treated as a quoting character by IE.
|
||||
'`': "`",
|
||||
}
|
||||
|
||||
// htmlNospaceNormReplacementTable is like htmlNospaceReplacementTable but
|
||||
// without '&' to avoid over-encoding existing entities.
|
||||
var htmlNospaceNormReplacementTable = []string{
|
||||
0: "�",
|
||||
'\t': "	",
|
||||
'\n': " ",
|
||||
'\v': "",
|
||||
'\f': "",
|
||||
'\r': " ",
|
||||
' ': " ",
|
||||
'"': """,
|
||||
'\'': "'",
|
||||
'+': "+",
|
||||
'<': "<",
|
||||
'=': "=",
|
||||
'>': ">",
|
||||
// A parse error in the attribute value (unquoted) and
|
||||
// before attribute value states.
|
||||
// Treated as a quoting character by IE.
|
||||
'`': "`",
|
||||
}
|
||||
|
||||
// htmlReplacer returns s with runes replaced according to replacementTable
|
||||
// and when badRunes is true, certain bad runes are allowed through unescaped.
|
||||
func htmlReplacer(s string, replacementTable []string, badRunes bool) string {
|
||||
written, b := 0, new(strings.Builder)
|
||||
r, w := rune(0), 0
|
||||
for i := 0; i < len(s); i += w {
|
||||
// Cannot use 'for range s' because we need to preserve the width
|
||||
// of the runes in the input. If we see a decoding error, the input
|
||||
// width will not be utf8.Runelen(r) and we will overrun the buffer.
|
||||
r, w = utf8.DecodeRuneInString(s[i:])
|
||||
if int(r) < len(replacementTable) {
|
||||
if repl := replacementTable[r]; len(repl) != 0 {
|
||||
if written == 0 {
|
||||
b.Grow(len(s))
|
||||
}
|
||||
b.WriteString(s[written:i])
|
||||
b.WriteString(repl)
|
||||
written = i + w
|
||||
}
|
||||
} else if badRunes {
|
||||
// No-op.
|
||||
// IE does not allow these ranges in unquoted attrs.
|
||||
} else if 0xfdd0 <= r && r <= 0xfdef || 0xfff0 <= r && r <= 0xffff {
|
||||
if written == 0 {
|
||||
b.Grow(len(s))
|
||||
}
|
||||
fmt.Fprintf(b, "%s&#x%x;", s[written:i], r)
|
||||
written = i + w
|
||||
}
|
||||
}
|
||||
if written == 0 {
|
||||
return s
|
||||
}
|
||||
b.WriteString(s[written:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// stripTags takes a snippet of HTML and returns only the text content.
|
||||
// For example, `<b>¡Hi!</b> <script>...</script>` -> `¡Hi! `.
|
||||
func stripTags(html string) string {
|
||||
var b bytes.Buffer
|
||||
s, c, i, allText := []byte(html), context{}, 0, true
|
||||
// Using the transition funcs helps us avoid mangling
|
||||
// `<div title="1>2">` or `I <3 Ponies!`.
|
||||
for i != len(s) {
|
||||
if c.delim == delimNone {
|
||||
st := c.state
|
||||
// Use RCDATA instead of parsing into JS or CSS styles.
|
||||
if c.element != elementNone && !isInTag(st) {
|
||||
st = stateRCDATA
|
||||
}
|
||||
d, nread := transitionFunc[st](c, s[i:])
|
||||
i1 := i + nread
|
||||
if c.state == stateText || c.state == stateRCDATA {
|
||||
// Emit text up to the start of the tag or comment.
|
||||
j := i1
|
||||
if d.state != c.state {
|
||||
for j1 := j - 1; j1 >= i; j1-- {
|
||||
if s[j1] == '<' {
|
||||
j = j1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Write(s[i:j])
|
||||
} else {
|
||||
allText = false
|
||||
}
|
||||
c, i = d, i1
|
||||
continue
|
||||
}
|
||||
i1 := i + bytes.IndexAny(s[i:], delimEnds[c.delim])
|
||||
if i1 < i {
|
||||
break
|
||||
}
|
||||
if c.delim != delimSpaceOrTagEnd {
|
||||
// Consume any quote.
|
||||
i1++
|
||||
}
|
||||
c, i = context{state: stateTag, element: c.element}, i1
|
||||
}
|
||||
if allText {
|
||||
return html
|
||||
} else if c.state == stateText || c.state == stateRCDATA {
|
||||
b.Write(s[i:])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// htmlNameFilter accepts valid parts of an HTML attribute or tag name or
|
||||
// a known-safe HTML attribute.
|
||||
func htmlNameFilter(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeHTMLAttr {
|
||||
return s
|
||||
}
|
||||
if len(s) == 0 {
|
||||
// Avoid violation of structure preservation.
|
||||
// <input checked {{.K}}={{.V}}>.
|
||||
// Without this, if .K is empty then .V is the value of
|
||||
// checked, but otherwise .V is the value of the attribute
|
||||
// named .K.
|
||||
return filterFailsafe
|
||||
}
|
||||
s = strings.ToLower(s)
|
||||
if t := attrType(s); t != contentTypePlain {
|
||||
// TODO: Split attr and element name part filters so we can whitelist
|
||||
// attributes.
|
||||
return filterFailsafe
|
||||
}
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
case 'a' <= r && r <= 'z':
|
||||
default:
|
||||
return filterFailsafe
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// commentEscaper returns the empty string regardless of input.
|
||||
// Comment content does not correspond to any parsed structure or
|
||||
// human-readable content, so the simplest and most secure policy is to drop
|
||||
// content interpolated into comments.
|
||||
// This approach is equally valid whether or not static comment content is
|
||||
// removed from the template.
|
||||
func commentEscaper(args ...interface{}) string {
|
||||
return ""
|
||||
}
|
99
tpl/internal/go_templates/htmltemplate/html_test.go
Normal file
99
tpl/internal/go_templates/htmltemplate/html_test.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"html"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTMLNospaceEscaper(t *testing.T) {
|
||||
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !"#$%&'()*+,-./` +
|
||||
`0123456789:;<=>?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\]^_` +
|
||||
"`abcdefghijklmno" +
|
||||
"pqrstuvwxyz{|}~\x7f" +
|
||||
"\u00A0\u0100\u2028\u2029\ufeff\ufdec\U0001D11E" +
|
||||
"erroneous\x960") // keep at the end
|
||||
|
||||
want := ("�\x01\x02\x03\x04\x05\x06\x07" +
|
||||
"\x08	  \x0E\x0F" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17" +
|
||||
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !"#$%&'()*+,-./` +
|
||||
`0123456789:;<=>?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\]^_` +
|
||||
``abcdefghijklmno` +
|
||||
`pqrstuvwxyz{|}~` + "\u007f" +
|
||||
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E" +
|
||||
"erroneous�0") // keep at the end
|
||||
|
||||
got := htmlNospaceEscaper(input)
|
||||
if got != want {
|
||||
t.Errorf("encode: want\n\t%q\nbut got\n\t%q", want, got)
|
||||
}
|
||||
|
||||
r := strings.NewReplacer("\x00", "\ufffd", "\x96", "\ufffd")
|
||||
got, want = html.UnescapeString(got), r.Replace(input)
|
||||
if want != got {
|
||||
t.Errorf("decode: want\n\t%q\nbut got\n\t%q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"Hello, World!", "Hello, World!"},
|
||||
{"foo&bar", "foo&bar"},
|
||||
{`Hello <a href="www.example.com/">World</a>!`, "Hello World!"},
|
||||
{"Foo <textarea>Bar</textarea> Baz", "Foo Bar Baz"},
|
||||
{"Foo <!-- Bar --> Baz", "Foo Baz"},
|
||||
{"<", "<"},
|
||||
{"foo < bar", "foo < bar"},
|
||||
{`Foo<script type="text/javascript">alert(1337)</script>Bar`, "FooBar"},
|
||||
{`Foo<div title="1>2">Bar`, "FooBar"},
|
||||
{`I <3 Ponies!`, `I <3 Ponies!`},
|
||||
{`<script>foo()</script>`, ``},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := stripTags(test.input); got != test.want {
|
||||
t.Errorf("%q: want %q, got %q", test.input, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHTMLNospaceEscaper(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
htmlNospaceEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHTMLNospaceEscaperNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
htmlNospaceEscaper("The_quick,_brown_fox_jumps_over_the_lazy_dog.")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStripTags(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
stripTags("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStripTagsNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
stripTags("The quick, brown fox jumps over the lazy dog.")
|
||||
}
|
||||
}
|
33
tpl/internal/go_templates/htmltemplate/hugo_template.go
Normal file
33
tpl/internal/go_templates/htmltemplate/hugo_template.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
This files contains the Hugo related addons. All the other files in this
|
||||
package is auto generated.
|
||||
|
||||
*/
|
||||
|
||||
// Prepare returns a template ready for execution.
|
||||
func (t *Template) Prepare() (*template.Template, error) {
|
||||
if err := t.escape(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.text, nil
|
||||
}
|
418
tpl/internal/go_templates/htmltemplate/js.go
Normal file
418
tpl/internal/go_templates/htmltemplate/js.go
Normal file
|
@ -0,0 +1,418 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
htmltemplate "html/template"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// nextJSCtx returns the context that determines whether a slash after the
|
||||
// given run of tokens starts a regular expression instead of a division
|
||||
// operator: / or /=.
|
||||
//
|
||||
// This assumes that the token run does not include any string tokens, comment
|
||||
// tokens, regular expression literal tokens, or division operators.
|
||||
//
|
||||
// This fails on some valid but nonsensical JavaScript programs like
|
||||
// "x = ++/foo/i" which is quite different than "x++/foo/i", but is not known to
|
||||
// fail on any known useful programs. It is based on the draft
|
||||
// JavaScript 2.0 lexical grammar and requires one token of lookbehind:
|
||||
// https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html
|
||||
func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
|
||||
s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029")
|
||||
if len(s) == 0 {
|
||||
return preceding
|
||||
}
|
||||
|
||||
// All cases below are in the single-byte UTF-8 group.
|
||||
switch c, n := s[len(s)-1], len(s); c {
|
||||
case '+', '-':
|
||||
// ++ and -- are not regexp preceders, but + and - are whether
|
||||
// they are used as infix or prefix operators.
|
||||
start := n - 1
|
||||
// Count the number of adjacent dashes or pluses.
|
||||
for start > 0 && s[start-1] == c {
|
||||
start--
|
||||
}
|
||||
if (n-start)&1 == 1 {
|
||||
// Reached for trailing minus signs since "---" is the
|
||||
// same as "-- -".
|
||||
return jsCtxRegexp
|
||||
}
|
||||
return jsCtxDivOp
|
||||
case '.':
|
||||
// Handle "42."
|
||||
if n != 1 && '0' <= s[n-2] && s[n-2] <= '9' {
|
||||
return jsCtxDivOp
|
||||
}
|
||||
return jsCtxRegexp
|
||||
// Suffixes for all punctuators from section 7.7 of the language spec
|
||||
// that only end binary operators not handled above.
|
||||
case ',', '<', '>', '=', '*', '%', '&', '|', '^', '?':
|
||||
return jsCtxRegexp
|
||||
// Suffixes for all punctuators from section 7.7 of the language spec
|
||||
// that are prefix operators not handled above.
|
||||
case '!', '~':
|
||||
return jsCtxRegexp
|
||||
// Matches all the punctuators from section 7.7 of the language spec
|
||||
// that are open brackets not handled above.
|
||||
case '(', '[':
|
||||
return jsCtxRegexp
|
||||
// Matches all the punctuators from section 7.7 of the language spec
|
||||
// that precede expression starts.
|
||||
case ':', ';', '{':
|
||||
return jsCtxRegexp
|
||||
// CAVEAT: the close punctuators ('}', ']', ')') precede div ops and
|
||||
// are handled in the default except for '}' which can precede a
|
||||
// division op as in
|
||||
// ({ valueOf: function () { return 42 } } / 2
|
||||
// which is valid, but, in practice, developers don't divide object
|
||||
// literals, so our heuristic works well for code like
|
||||
// function () { ... } /foo/.test(x) && sideEffect();
|
||||
// The ')' punctuator can precede a regular expression as in
|
||||
// if (b) /foo/.test(x) && ...
|
||||
// but this is much less likely than
|
||||
// (a + b) / c
|
||||
case '}':
|
||||
return jsCtxRegexp
|
||||
default:
|
||||
// Look for an IdentifierName and see if it is a keyword that
|
||||
// can precede a regular expression.
|
||||
j := n
|
||||
for j > 0 && isJSIdentPart(rune(s[j-1])) {
|
||||
j--
|
||||
}
|
||||
if regexpPrecederKeywords[string(s[j:])] {
|
||||
return jsCtxRegexp
|
||||
}
|
||||
}
|
||||
// Otherwise is a punctuator not listed above, or
|
||||
// a string which precedes a div op, or an identifier
|
||||
// which precedes a div op.
|
||||
return jsCtxDivOp
|
||||
}
|
||||
|
||||
// regexpPrecederKeywords is a set of reserved JS keywords that can precede a
|
||||
// regular expression in JS source.
|
||||
var regexpPrecederKeywords = map[string]bool{
|
||||
"break": true,
|
||||
"case": true,
|
||||
"continue": true,
|
||||
"delete": true,
|
||||
"do": true,
|
||||
"else": true,
|
||||
"finally": true,
|
||||
"in": true,
|
||||
"instanceof": true,
|
||||
"return": true,
|
||||
"throw": true,
|
||||
"try": true,
|
||||
"typeof": true,
|
||||
"void": true,
|
||||
}
|
||||
|
||||
var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
|
||||
|
||||
// indirectToJSONMarshaler returns the value, after dereferencing as many times
|
||||
// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
|
||||
func indirectToJSONMarshaler(a interface{}) interface{} {
|
||||
// text/template now supports passing untyped nil as a func call
|
||||
// argument, so we must support it. Otherwise we'd panic below, as one
|
||||
// cannot call the Type or Interface methods on an invalid
|
||||
// reflect.Value. See golang.org/issue/18716.
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(a)
|
||||
for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Ptr && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
|
||||
// neither side-effects nor free variables outside (NaN, Infinity).
|
||||
func jsValEscaper(args ...interface{}) string {
|
||||
var a interface{}
|
||||
if len(args) == 1 {
|
||||
a = indirectToJSONMarshaler(args[0])
|
||||
switch t := a.(type) {
|
||||
case htmltemplate.JS:
|
||||
return string(t)
|
||||
case htmltemplate.JSStr:
|
||||
// TODO: normalize quotes.
|
||||
return `"` + string(t) + `"`
|
||||
case json.Marshaler:
|
||||
// Do not treat as a Stringer.
|
||||
case fmt.Stringer:
|
||||
a = t.String()
|
||||
}
|
||||
} else {
|
||||
for i, arg := range args {
|
||||
args[i] = indirectToJSONMarshaler(arg)
|
||||
}
|
||||
a = fmt.Sprint(args...)
|
||||
}
|
||||
// TODO: detect cycles before calling Marshal which loops infinitely on
|
||||
// cyclic data. This may be an unacceptable DoS risk.
|
||||
|
||||
b, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
// Put a space before comment so that if it is flush against
|
||||
// a division operator it is not turned into a line comment:
|
||||
// x/{{y}}
|
||||
// turning into
|
||||
// x//* error marshaling y:
|
||||
// second line of error message */null
|
||||
return fmt.Sprintf(" /* %s */null ", strings.ReplaceAll(err.Error(), "*/", "* /"))
|
||||
}
|
||||
|
||||
// TODO: maybe post-process output to prevent it from containing
|
||||
// "<!--", "-->", "<![CDATA[", "]]>", or "</script"
|
||||
// in case custom marshalers produce output containing those.
|
||||
|
||||
// TODO: Maybe abbreviate \u00ab to \xab to produce more compact output.
|
||||
if len(b) == 0 {
|
||||
// In, `x=y/{{.}}*z` a json.Marshaler that produces "" should
|
||||
// not cause the output `x=y/*z`.
|
||||
return " null "
|
||||
}
|
||||
first, _ := utf8.DecodeRune(b)
|
||||
last, _ := utf8.DecodeLastRune(b)
|
||||
var buf strings.Builder
|
||||
// Prevent IdentifierNames and NumericLiterals from running into
|
||||
// keywords: in, instanceof, typeof, void
|
||||
pad := isJSIdentPart(first) || isJSIdentPart(last)
|
||||
if pad {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
written := 0
|
||||
// Make sure that json.Marshal escapes codepoints U+2028 & U+2029
|
||||
// so it falls within the subset of JSON which is valid JS.
|
||||
for i := 0; i < len(b); {
|
||||
rune, n := utf8.DecodeRune(b[i:])
|
||||
repl := ""
|
||||
if rune == 0x2028 {
|
||||
repl = `\u2028`
|
||||
} else if rune == 0x2029 {
|
||||
repl = `\u2029`
|
||||
}
|
||||
if repl != "" {
|
||||
buf.Write(b[written:i])
|
||||
buf.WriteString(repl)
|
||||
written = i + n
|
||||
}
|
||||
i += n
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
buf.Write(b[written:])
|
||||
if pad {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// jsStrEscaper produces a string that can be included between quotes in
|
||||
// JavaScript source, in JavaScript embedded in an HTML5 <script> element,
|
||||
// or in an HTML5 event handler attribute such as onclick.
|
||||
func jsStrEscaper(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeJSStr {
|
||||
return replace(s, jsStrNormReplacementTable)
|
||||
}
|
||||
return replace(s, jsStrReplacementTable)
|
||||
}
|
||||
|
||||
// jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression
|
||||
// specials so the result is treated literally when included in a regular
|
||||
// expression literal. /foo{{.X}}bar/ matches the string "foo" followed by
|
||||
// the literal text of {{.X}} followed by the string "bar".
|
||||
func jsRegexpEscaper(args ...interface{}) string {
|
||||
s, _ := stringify(args...)
|
||||
s = replace(s, jsRegexpReplacementTable)
|
||||
if s == "" {
|
||||
// /{{.X}}/ should not produce a line comment when .X == "".
|
||||
return "(?:)"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// replace replaces each rune r of s with replacementTable[r], provided that
|
||||
// r < len(replacementTable). If replacementTable[r] is the empty string then
|
||||
// no replacement is made.
|
||||
// It also replaces runes U+2028 and U+2029 with the raw strings `\u2028` and
|
||||
// `\u2029`.
|
||||
func replace(s string, replacementTable []string) string {
|
||||
var b strings.Builder
|
||||
r, w, written := rune(0), 0, 0
|
||||
for i := 0; i < len(s); i += w {
|
||||
// See comment in htmlEscaper.
|
||||
r, w = utf8.DecodeRuneInString(s[i:])
|
||||
var repl string
|
||||
switch {
|
||||
case int(r) < len(replacementTable) && replacementTable[r] != "":
|
||||
repl = replacementTable[r]
|
||||
case r == '\u2028':
|
||||
repl = `\u2028`
|
||||
case r == '\u2029':
|
||||
repl = `\u2029`
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if written == 0 {
|
||||
b.Grow(len(s))
|
||||
}
|
||||
b.WriteString(s[written:i])
|
||||
b.WriteString(repl)
|
||||
written = i + w
|
||||
}
|
||||
if written == 0 {
|
||||
return s
|
||||
}
|
||||
b.WriteString(s[written:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var jsStrReplacementTable = []string{
|
||||
0: `\0`,
|
||||
'\t': `\t`,
|
||||
'\n': `\n`,
|
||||
'\v': `\x0b`, // "\v" == "v" on IE 6.
|
||||
'\f': `\f`,
|
||||
'\r': `\r`,
|
||||
// Encode HTML specials as hex so the output can be embedded
|
||||
// in HTML attributes without further encoding.
|
||||
'"': `\x22`,
|
||||
'&': `\x26`,
|
||||
'\'': `\x27`,
|
||||
'+': `\x2b`,
|
||||
'/': `\/`,
|
||||
'<': `\x3c`,
|
||||
'>': `\x3e`,
|
||||
'\\': `\\`,
|
||||
}
|
||||
|
||||
// jsStrNormReplacementTable is like jsStrReplacementTable but does not
|
||||
// overencode existing escapes since this table has no entry for `\`.
|
||||
var jsStrNormReplacementTable = []string{
|
||||
0: `\0`,
|
||||
'\t': `\t`,
|
||||
'\n': `\n`,
|
||||
'\v': `\x0b`, // "\v" == "v" on IE 6.
|
||||
'\f': `\f`,
|
||||
'\r': `\r`,
|
||||
// Encode HTML specials as hex so the output can be embedded
|
||||
// in HTML attributes without further encoding.
|
||||
'"': `\x22`,
|
||||
'&': `\x26`,
|
||||
'\'': `\x27`,
|
||||
'+': `\x2b`,
|
||||
'/': `\/`,
|
||||
'<': `\x3c`,
|
||||
'>': `\x3e`,
|
||||
}
|
||||
|
||||
var jsRegexpReplacementTable = []string{
|
||||
0: `\0`,
|
||||
'\t': `\t`,
|
||||
'\n': `\n`,
|
||||
'\v': `\x0b`, // "\v" == "v" on IE 6.
|
||||
'\f': `\f`,
|
||||
'\r': `\r`,
|
||||
// Encode HTML specials as hex so the output can be embedded
|
||||
// in HTML attributes without further encoding.
|
||||
'"': `\x22`,
|
||||
'$': `\$`,
|
||||
'&': `\x26`,
|
||||
'\'': `\x27`,
|
||||
'(': `\(`,
|
||||
')': `\)`,
|
||||
'*': `\*`,
|
||||
'+': `\x2b`,
|
||||
'-': `\-`,
|
||||
'.': `\.`,
|
||||
'/': `\/`,
|
||||
'<': `\x3c`,
|
||||
'>': `\x3e`,
|
||||
'?': `\?`,
|
||||
'[': `\[`,
|
||||
'\\': `\\`,
|
||||
']': `\]`,
|
||||
'^': `\^`,
|
||||
'{': `\{`,
|
||||
'|': `\|`,
|
||||
'}': `\}`,
|
||||
}
|
||||
|
||||
// isJSIdentPart reports whether the given rune is a JS identifier part.
|
||||
// It does not handle all the non-Latin letters, joiners, and combining marks,
|
||||
// but it does handle every codepoint that can occur in a numeric literal or
|
||||
// a keyword.
|
||||
func isJSIdentPart(r rune) bool {
|
||||
switch {
|
||||
case r == '$':
|
||||
return true
|
||||
case '0' <= r && r <= '9':
|
||||
return true
|
||||
case 'A' <= r && r <= 'Z':
|
||||
return true
|
||||
case r == '_':
|
||||
return true
|
||||
case 'a' <= r && r <= 'z':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isJSType reports whether the given MIME type should be considered JavaScript.
|
||||
//
|
||||
// It is used to determine whether a script tag with a type attribute is a javascript container.
|
||||
func isJSType(mimeType string) bool {
|
||||
// per
|
||||
// https://www.w3.org/TR/html5/scripting-1.html#attr-script-type
|
||||
// https://tools.ietf.org/html/rfc7231#section-3.1.1
|
||||
// https://tools.ietf.org/html/rfc4329#section-3
|
||||
// https://www.ietf.org/rfc/rfc4627.txt
|
||||
mimeType = strings.ToLower(mimeType)
|
||||
// discard parameters
|
||||
if i := strings.Index(mimeType, ";"); i >= 0 {
|
||||
mimeType = mimeType[:i]
|
||||
}
|
||||
mimeType = strings.TrimSpace(mimeType)
|
||||
switch mimeType {
|
||||
case
|
||||
"application/ecmascript",
|
||||
"application/javascript",
|
||||
"application/json",
|
||||
"application/ld+json",
|
||||
"application/x-ecmascript",
|
||||
"application/x-javascript",
|
||||
"module",
|
||||
"text/ecmascript",
|
||||
"text/javascript",
|
||||
"text/javascript1.0",
|
||||
"text/javascript1.1",
|
||||
"text/javascript1.2",
|
||||
"text/javascript1.3",
|
||||
"text/javascript1.4",
|
||||
"text/javascript1.5",
|
||||
"text/jscript",
|
||||
"text/livescript",
|
||||
"text/x-ecmascript",
|
||||
"text/x-javascript":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
425
tpl/internal/go_templates/htmltemplate/js_test.go
Normal file
425
tpl/internal/go_templates/htmltemplate/js_test.go
Normal file
|
@ -0,0 +1,425 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNextJsCtx(t *testing.T) {
|
||||
tests := []struct {
|
||||
jsCtx jsCtx
|
||||
s string
|
||||
}{
|
||||
// Statement terminators precede regexps.
|
||||
{jsCtxRegexp, ";"},
|
||||
// This is not airtight.
|
||||
// ({ valueOf: function () { return 1 } } / 2)
|
||||
// is valid JavaScript but in practice, devs do not do this.
|
||||
// A block followed by a statement starting with a RegExp is
|
||||
// much more common:
|
||||
// while (x) {...} /foo/.test(x) || panic()
|
||||
{jsCtxRegexp, "}"},
|
||||
// But member, call, grouping, and array expression terminators
|
||||
// precede div ops.
|
||||
{jsCtxDivOp, ")"},
|
||||
{jsCtxDivOp, "]"},
|
||||
// At the start of a primary expression, array, or expression
|
||||
// statement, expect a regexp.
|
||||
{jsCtxRegexp, "("},
|
||||
{jsCtxRegexp, "["},
|
||||
{jsCtxRegexp, "{"},
|
||||
// Assignment operators precede regexps as do all exclusively
|
||||
// prefix and binary operators.
|
||||
{jsCtxRegexp, "="},
|
||||
{jsCtxRegexp, "+="},
|
||||
{jsCtxRegexp, "*="},
|
||||
{jsCtxRegexp, "*"},
|
||||
{jsCtxRegexp, "!"},
|
||||
// Whether the + or - is infix or prefix, it cannot precede a
|
||||
// div op.
|
||||
{jsCtxRegexp, "+"},
|
||||
{jsCtxRegexp, "-"},
|
||||
// An incr/decr op precedes a div operator.
|
||||
// This is not airtight. In (g = ++/h/i) a regexp follows a
|
||||
// pre-increment operator, but in practice devs do not try to
|
||||
// increment or decrement regular expressions.
|
||||
// (g++/h/i) where ++ is a postfix operator on g is much more
|
||||
// common.
|
||||
{jsCtxDivOp, "--"},
|
||||
{jsCtxDivOp, "++"},
|
||||
{jsCtxDivOp, "x--"},
|
||||
// When we have many dashes or pluses, then they are grouped
|
||||
// left to right.
|
||||
{jsCtxRegexp, "x---"}, // A postfix -- then a -.
|
||||
// return followed by a slash returns the regexp literal or the
|
||||
// slash starts a regexp literal in an expression statement that
|
||||
// is dead code.
|
||||
{jsCtxRegexp, "return"},
|
||||
{jsCtxRegexp, "return "},
|
||||
{jsCtxRegexp, "return\t"},
|
||||
{jsCtxRegexp, "return\n"},
|
||||
{jsCtxRegexp, "return\u2028"},
|
||||
// Identifiers can be divided and cannot validly be preceded by
|
||||
// a regular expressions. Semicolon insertion cannot happen
|
||||
// between an identifier and a regular expression on a new line
|
||||
// because the one token lookahead for semicolon insertion has
|
||||
// to conclude that it could be a div binary op and treat it as
|
||||
// such.
|
||||
{jsCtxDivOp, "x"},
|
||||
{jsCtxDivOp, "x "},
|
||||
{jsCtxDivOp, "x\t"},
|
||||
{jsCtxDivOp, "x\n"},
|
||||
{jsCtxDivOp, "x\u2028"},
|
||||
{jsCtxDivOp, "preturn"},
|
||||
// Numbers precede div ops.
|
||||
{jsCtxDivOp, "0"},
|
||||
// Dots that are part of a number are div preceders.
|
||||
{jsCtxDivOp, "0."},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx {
|
||||
t.Errorf("want %s got %q", test.jsCtx, test.s)
|
||||
}
|
||||
if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx {
|
||||
t.Errorf("want %s got %q", test.jsCtx, test.s)
|
||||
}
|
||||
}
|
||||
|
||||
if nextJSCtx([]byte(" "), jsCtxRegexp) != jsCtxRegexp {
|
||||
t.Error("Blank tokens")
|
||||
}
|
||||
|
||||
if nextJSCtx([]byte(" "), jsCtxDivOp) != jsCtxDivOp {
|
||||
t.Error("Blank tokens")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSValEscaper(t *testing.T) {
|
||||
tests := []struct {
|
||||
x interface{}
|
||||
js string
|
||||
}{
|
||||
{int(42), " 42 "},
|
||||
{uint(42), " 42 "},
|
||||
{int16(42), " 42 "},
|
||||
{uint16(42), " 42 "},
|
||||
{int32(-42), " -42 "},
|
||||
{uint32(42), " 42 "},
|
||||
{int16(-42), " -42 "},
|
||||
{uint16(42), " 42 "},
|
||||
{int64(-42), " -42 "},
|
||||
{uint64(42), " 42 "},
|
||||
{uint64(1) << 53, " 9007199254740992 "},
|
||||
// ulp(1 << 53) > 1 so this loses precision in JS
|
||||
// but it is still a representable integer literal.
|
||||
{uint64(1)<<53 + 1, " 9007199254740993 "},
|
||||
{float32(1.0), " 1 "},
|
||||
{float32(-1.0), " -1 "},
|
||||
{float32(0.5), " 0.5 "},
|
||||
{float32(-0.5), " -0.5 "},
|
||||
{float32(1.0) / float32(256), " 0.00390625 "},
|
||||
{float32(0), " 0 "},
|
||||
{math.Copysign(0, -1), " -0 "},
|
||||
{float64(1.0), " 1 "},
|
||||
{float64(-1.0), " -1 "},
|
||||
{float64(0.5), " 0.5 "},
|
||||
{float64(-0.5), " -0.5 "},
|
||||
{float64(0), " 0 "},
|
||||
{math.Copysign(0, -1), " -0 "},
|
||||
{"", `""`},
|
||||
{"foo", `"foo"`},
|
||||
// Newlines.
|
||||
{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
|
||||
// "\v" == "v" on IE 6 so use "\x0b" instead.
|
||||
{"\t\x0b", `"\t\u000b"`},
|
||||
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
|
||||
{[]interface{}{}, "[]"},
|
||||
{[]interface{}{42, "foo", nil}, `[42,"foo",null]`},
|
||||
{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`},
|
||||
{"<!--", `"\u003c!--"`},
|
||||
{"-->", `"--\u003e"`},
|
||||
{"<![CDATA[", `"\u003c![CDATA["`},
|
||||
{"]]>", `"]]\u003e"`},
|
||||
{"</script", `"\u003c/script"`},
|
||||
{"\U0001D11E", "\"\U0001D11E\""}, // or "\uD834\uDD1E"
|
||||
{nil, " null "},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if js := jsValEscaper(test.x); js != test.js {
|
||||
t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js)
|
||||
}
|
||||
// Make sure that escaping corner cases are not broken
|
||||
// by nesting.
|
||||
a := []interface{}{test.x}
|
||||
want := "[" + strings.TrimSpace(test.js) + "]"
|
||||
if js := jsValEscaper(a); js != want {
|
||||
t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSStrEscaper(t *testing.T) {
|
||||
tests := []struct {
|
||||
x interface{}
|
||||
esc string
|
||||
}{
|
||||
{"", ``},
|
||||
{"foo", `foo`},
|
||||
{"\u0000", `\0`},
|
||||
{"\t", `\t`},
|
||||
{"\n", `\n`},
|
||||
{"\r", `\r`},
|
||||
{"\u2028", `\u2028`},
|
||||
{"\u2029", `\u2029`},
|
||||
{"\\", `\\`},
|
||||
{"\\n", `\\n`},
|
||||
{"foo\r\nbar", `foo\r\nbar`},
|
||||
// Preserve attribute boundaries.
|
||||
{`"`, `\x22`},
|
||||
{`'`, `\x27`},
|
||||
// Allow embedding in HTML without further escaping.
|
||||
{`&`, `\x26amp;`},
|
||||
// Prevent breaking out of text node and element boundaries.
|
||||
{"</script>", `\x3c\/script\x3e`},
|
||||
{"<![CDATA[", `\x3c![CDATA[`},
|
||||
{"]]>", `]]\x3e`},
|
||||
// https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
|
||||
// "The text in style, script, title, and textarea elements
|
||||
// must not have an escaping text span start that is not
|
||||
// followed by an escaping text span end."
|
||||
// Furthermore, spoofing an escaping text span end could lead
|
||||
// to different interpretation of a </script> sequence otherwise
|
||||
// masked by the escaping text span, and spoofing a start could
|
||||
// allow regular text content to be interpreted as script
|
||||
// allowing script execution via a combination of a JS string
|
||||
// injection followed by an HTML text injection.
|
||||
{"<!--", `\x3c!--`},
|
||||
{"-->", `--\x3e`},
|
||||
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
|
||||
{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
|
||||
`\x2bADw-script\x2bAD4-alert(1)\x2bADw-\/script\x2bAD4-`,
|
||||
},
|
||||
// Invalid UTF-8 sequence
|
||||
{"foo\xA0bar", "foo\xA0bar"},
|
||||
// Invalid unicode scalar value.
|
||||
{"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
esc := jsStrEscaper(test.x)
|
||||
if esc != test.esc {
|
||||
t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSRegexpEscaper(t *testing.T) {
|
||||
tests := []struct {
|
||||
x interface{}
|
||||
esc string
|
||||
}{
|
||||
{"", `(?:)`},
|
||||
{"foo", `foo`},
|
||||
{"\u0000", `\0`},
|
||||
{"\t", `\t`},
|
||||
{"\n", `\n`},
|
||||
{"\r", `\r`},
|
||||
{"\u2028", `\u2028`},
|
||||
{"\u2029", `\u2029`},
|
||||
{"\\", `\\`},
|
||||
{"\\n", `\\n`},
|
||||
{"foo\r\nbar", `foo\r\nbar`},
|
||||
// Preserve attribute boundaries.
|
||||
{`"`, `\x22`},
|
||||
{`'`, `\x27`},
|
||||
// Allow embedding in HTML without further escaping.
|
||||
{`&`, `\x26amp;`},
|
||||
// Prevent breaking out of text node and element boundaries.
|
||||
{"</script>", `\x3c\/script\x3e`},
|
||||
{"<![CDATA[", `\x3c!\[CDATA\[`},
|
||||
{"]]>", `\]\]\x3e`},
|
||||
// Escaping text spans.
|
||||
{"<!--", `\x3c!\-\-`},
|
||||
{"-->", `\-\-\x3e`},
|
||||
{"*", `\*`},
|
||||
{"+", `\x2b`},
|
||||
{"?", `\?`},
|
||||
{"[](){}", `\[\]\(\)\{\}`},
|
||||
{"$foo|x.y", `\$foo\|x\.y`},
|
||||
{"x^y", `x\^y`},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
esc := jsRegexpEscaper(test.x)
|
||||
if esc != test.esc {
|
||||
t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
|
||||
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !"#$%&'()*+,-./` +
|
||||
`0123456789:;<=>?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\]^_` +
|
||||
"`abcdefghijklmno" +
|
||||
"pqrstuvwxyz{|}~\x7f" +
|
||||
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
escaper func(...interface{}) string
|
||||
escaped string
|
||||
}{
|
||||
{
|
||||
"jsStrEscaper",
|
||||
jsStrEscaper,
|
||||
"\\0\x01\x02\x03\x04\x05\x06\x07" +
|
||||
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17" +
|
||||
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !\x22#$%\x26\x27()*\x2b,-.\/` +
|
||||
`0123456789:;\x3c=\x3e?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\\]^_` +
|
||||
"`abcdefghijklmno" +
|
||||
"pqrstuvwxyz{|}~\x7f" +
|
||||
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
|
||||
},
|
||||
{
|
||||
"jsRegexpEscaper",
|
||||
jsRegexpEscaper,
|
||||
"\\0\x01\x02\x03\x04\x05\x06\x07" +
|
||||
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17" +
|
||||
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !\x22#\$%\x26\x27\(\)\*\x2b,\-\.\/` +
|
||||
`0123456789:;\x3c=\x3e\?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ\[\\\]\^_` +
|
||||
"`abcdefghijklmno" +
|
||||
`pqrstuvwxyz\{\|\}~` + "\u007f" +
|
||||
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if s := test.escaper(input); s != test.escaped {
|
||||
t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
|
||||
continue
|
||||
}
|
||||
|
||||
// Escape it rune by rune to make sure that any
|
||||
// fast-path checking does not break escaping.
|
||||
var buf bytes.Buffer
|
||||
for _, c := range input {
|
||||
buf.WriteString(test.escaper(string(c)))
|
||||
}
|
||||
|
||||
if s := buf.String(); s != test.escaped {
|
||||
t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsJsMimeType(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
out bool
|
||||
}{
|
||||
{"application/javascript;version=1.8", true},
|
||||
{"application/javascript;version=1.8;foo=bar", true},
|
||||
{"application/javascript/version=1.8", false},
|
||||
{"text/javascript", true},
|
||||
{"application/json", true},
|
||||
{"application/ld+json", true},
|
||||
{"module", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if isJSType(test.in) != test.out {
|
||||
t.Errorf("isJSType(%q) = %v, want %v", test.in, !test.out, test.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSValEscaperWithNum(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsValEscaper(3.141592654)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSValEscaperWithStr(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsValEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsValEscaper("The quick, brown fox jumps over the lazy dog")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSValEscaperWithObj(b *testing.B) {
|
||||
o := struct {
|
||||
S string
|
||||
N int
|
||||
}{
|
||||
"The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>\u2028",
|
||||
42,
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsValEscaper(o)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) {
|
||||
o := struct {
|
||||
S string
|
||||
N int
|
||||
}{
|
||||
"The quick, brown fox jumps over the lazy dog",
|
||||
42,
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsValEscaper(o)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSStrEscaperNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsStrEscaper("The quick, brown fox jumps over the lazy dog.")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSStrEscaper(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsStrEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsRegexpEscaper("The quick, brown fox jumps over the lazy dog")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSRegexpEscaper(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
jsRegexpEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
|
||||
}
|
||||
}
|
16
tpl/internal/go_templates/htmltemplate/jsctx_string.go
Normal file
16
tpl/internal/go_templates/htmltemplate/jsctx_string.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type jsCtx"; DO NOT EDIT.
|
||||
|
||||
package template
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
|
||||
|
||||
var _jsCtx_index = [...]uint8{0, 11, 21, 33}
|
||||
|
||||
func (i jsCtx) String() string {
|
||||
if i >= jsCtx(len(_jsCtx_index)-1) {
|
||||
return "jsCtx(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _jsCtx_name[_jsCtx_index[i]:_jsCtx_index[i+1]]
|
||||
}
|
16
tpl/internal/go_templates/htmltemplate/state_string.go
Normal file
16
tpl/internal/go_templates/htmltemplate/state_string.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type state"; DO NOT EDIT.
|
||||
|
||||
package template
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateError"
|
||||
|
||||
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 155, 170, 184, 192, 205, 218, 231, 244, 255, 271, 286, 296}
|
||||
|
||||
func (i state) String() string {
|
||||
if i >= state(len(_state_index)-1) {
|
||||
return "state(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _state_name[_state_index[i]:_state_index[i+1]]
|
||||
}
|
491
tpl/internal/go_templates/htmltemplate/template.go
Normal file
491
tpl/internal/go_templates/htmltemplate/template.go
Normal file
|
@ -0,0 +1,491 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
// Template is a specialized Template from "text/template" that produces a safe
|
||||
// HTML document fragment.
|
||||
type Template struct {
|
||||
// Sticky error if escaping fails, or escapeOK if succeeded.
|
||||
escapeErr error
|
||||
// We could embed the text/template field, but it's safer not to because
|
||||
// we need to keep our version of the name space and the underlying
|
||||
// template's in sync.
|
||||
text *template.Template
|
||||
// The underlying template's parse tree, updated to be HTML-safe.
|
||||
Tree *parse.Tree
|
||||
*nameSpace // common to all associated templates
|
||||
}
|
||||
|
||||
// escapeOK is a sentinel value used to indicate valid escaping.
|
||||
var escapeOK = fmt.Errorf("template escaped correctly")
|
||||
|
||||
// nameSpace is the data structure shared by all templates in an association.
|
||||
type nameSpace struct {
|
||||
mu sync.Mutex
|
||||
set map[string]*Template
|
||||
escaped bool
|
||||
esc escaper
|
||||
}
|
||||
|
||||
// Templates returns a slice of the templates associated with t, including t
|
||||
// itself.
|
||||
func (t *Template) Templates() []*Template {
|
||||
ns := t.nameSpace
|
||||
ns.mu.Lock()
|
||||
defer ns.mu.Unlock()
|
||||
// Return a slice so we don't expose the map.
|
||||
m := make([]*Template, 0, len(ns.set))
|
||||
for _, v := range ns.set {
|
||||
m = append(m, v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Option sets options for the template. Options are described by
|
||||
// strings, either a simple string or "key=value". There can be at
|
||||
// most one equals sign in an option string. If the option string
|
||||
// is unrecognized or otherwise invalid, Option panics.
|
||||
//
|
||||
// Known options:
|
||||
//
|
||||
// missingkey: Control the behavior during execution if a map is
|
||||
// indexed with a key that is not present in the map.
|
||||
// "missingkey=default" or "missingkey=invalid"
|
||||
// The default behavior: Do nothing and continue execution.
|
||||
// If printed, the result of the index operation is the string
|
||||
// "<no value>".
|
||||
// "missingkey=zero"
|
||||
// The operation returns the zero value for the map type's element.
|
||||
// "missingkey=error"
|
||||
// Execution stops immediately with an error.
|
||||
//
|
||||
func (t *Template) Option(opt ...string) *Template {
|
||||
t.text.Option(opt...)
|
||||
return t
|
||||
}
|
||||
|
||||
// checkCanParse checks whether it is OK to parse templates.
|
||||
// If not, it returns an error.
|
||||
func (t *Template) checkCanParse() error {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
if t.nameSpace.escaped {
|
||||
return fmt.Errorf("html/template: cannot Parse after Execute")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// escape escapes all associated templates.
|
||||
func (t *Template) escape() error {
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
t.nameSpace.escaped = true
|
||||
if t.escapeErr == nil {
|
||||
if t.Tree == nil {
|
||||
return fmt.Errorf("template: %q is an incomplete or empty template", t.Name())
|
||||
}
|
||||
if err := escapeTemplate(t, t.text.Root, t.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if t.escapeErr != escapeOK {
|
||||
return t.escapeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute applies a parsed template to the specified data object,
|
||||
// writing the output to wr.
|
||||
// If an error occurs executing the template or writing its output,
|
||||
// execution stops, but partial results may already have been written to
|
||||
// the output writer.
|
||||
// A template may be executed safely in parallel, although if parallel
|
||||
// executions share a Writer the output may be interleaved.
|
||||
func (t *Template) Execute(wr io.Writer, data interface{}) error {
|
||||
if err := t.escape(); err != nil {
|
||||
return err
|
||||
}
|
||||
return t.text.Execute(wr, data)
|
||||
}
|
||||
|
||||
// ExecuteTemplate applies the template associated with t that has the given
|
||||
// name to the specified data object and writes the output to wr.
|
||||
// If an error occurs executing the template or writing its output,
|
||||
// execution stops, but partial results may already have been written to
|
||||
// the output writer.
|
||||
// A template may be executed safely in parallel, although if parallel
|
||||
// executions share a Writer the output may be interleaved.
|
||||
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
|
||||
tmpl, err := t.lookupAndEscapeTemplate(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tmpl.text.Execute(wr, data)
|
||||
}
|
||||
|
||||
// lookupAndEscapeTemplate guarantees that the template with the given name
|
||||
// is escaped, or returns an error if it cannot be. It returns the named
|
||||
// template.
|
||||
func (t *Template) lookupAndEscapeTemplate(name string) (tmpl *Template, err error) {
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
t.nameSpace.escaped = true
|
||||
tmpl = t.set[name]
|
||||
if tmpl == nil {
|
||||
return nil, fmt.Errorf("html/template: %q is undefined", name)
|
||||
}
|
||||
if tmpl.escapeErr != nil && tmpl.escapeErr != escapeOK {
|
||||
return nil, tmpl.escapeErr
|
||||
}
|
||||
if tmpl.text.Tree == nil || tmpl.text.Root == nil {
|
||||
return nil, fmt.Errorf("html/template: %q is an incomplete template", name)
|
||||
}
|
||||
if t.text.Lookup(name) == nil {
|
||||
panic("html/template internal error: template escaping out of sync")
|
||||
}
|
||||
if tmpl.escapeErr == nil {
|
||||
err = escapeTemplate(tmpl, tmpl.text.Root, name)
|
||||
}
|
||||
return tmpl, err
|
||||
}
|
||||
|
||||
// DefinedTemplates returns a string listing the defined templates,
|
||||
// prefixed by the string "; defined templates are: ". If there are none,
|
||||
// it returns the empty string. Used to generate an error message.
|
||||
func (t *Template) DefinedTemplates() string {
|
||||
return t.text.DefinedTemplates()
|
||||
}
|
||||
|
||||
// Parse parses text as a template body for t.
|
||||
// Named template definitions ({{define ...}} or {{block ...}} statements) in text
|
||||
// define additional templates associated with t and are removed from the
|
||||
// definition of t itself.
|
||||
//
|
||||
// Templates can be redefined in successive calls to Parse,
|
||||
// before the first use of Execute on t or any associated template.
|
||||
// A template definition with a body containing only white space and comments
|
||||
// is considered empty and will not replace an existing template's body.
|
||||
// This allows using Parse to add new named template definitions without
|
||||
// overwriting the main template body.
|
||||
func (t *Template) Parse(text string) (*Template, error) {
|
||||
if err := t.checkCanParse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret, err := t.text.Parse(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// In general, all the named templates might have changed underfoot.
|
||||
// Regardless, some new ones may have been defined.
|
||||
// The template.Template set has been updated; update ours.
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
for _, v := range ret.Templates() {
|
||||
name := v.Name()
|
||||
tmpl := t.set[name]
|
||||
if tmpl == nil {
|
||||
tmpl = t.new(name)
|
||||
}
|
||||
tmpl.text = v
|
||||
tmpl.Tree = v.Tree
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// AddParseTree creates a new template with the name and parse tree
|
||||
// and associates it with t.
|
||||
//
|
||||
// It returns an error if t or any associated template has already been executed.
|
||||
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
|
||||
if err := t.checkCanParse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
text, err := t.text.AddParseTree(name, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &Template{
|
||||
nil,
|
||||
text,
|
||||
text.Tree,
|
||||
t.nameSpace,
|
||||
}
|
||||
t.set[name] = ret
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Clone returns a duplicate of the template, including all associated
|
||||
// templates. The actual representation is not copied, but the name space of
|
||||
// associated templates is, so further calls to Parse in the copy will add
|
||||
// templates to the copy but not to the original. Clone can be used to prepare
|
||||
// common templates and use them with variant definitions for other templates
|
||||
// by adding the variants after the clone is made.
|
||||
//
|
||||
// It returns an error if t has already been executed.
|
||||
func (t *Template) Clone() (*Template, error) {
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
if t.escapeErr != nil {
|
||||
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
|
||||
}
|
||||
textClone, err := t.text.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ns := &nameSpace{set: make(map[string]*Template)}
|
||||
ns.esc = makeEscaper(ns)
|
||||
ret := &Template{
|
||||
nil,
|
||||
textClone,
|
||||
textClone.Tree,
|
||||
ns,
|
||||
}
|
||||
ret.set[ret.Name()] = ret
|
||||
for _, x := range textClone.Templates() {
|
||||
name := x.Name()
|
||||
src := t.set[name]
|
||||
if src == nil || src.escapeErr != nil {
|
||||
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
|
||||
}
|
||||
x.Tree = x.Tree.Copy()
|
||||
ret.set[name] = &Template{
|
||||
nil,
|
||||
x,
|
||||
x.Tree,
|
||||
ret.nameSpace,
|
||||
}
|
||||
}
|
||||
// Return the template associated with the name of this template.
|
||||
return ret.set[ret.Name()], nil
|
||||
}
|
||||
|
||||
// New allocates a new HTML template with the given name.
|
||||
func New(name string) *Template {
|
||||
ns := &nameSpace{set: make(map[string]*Template)}
|
||||
ns.esc = makeEscaper(ns)
|
||||
tmpl := &Template{
|
||||
nil,
|
||||
template.New(name),
|
||||
nil,
|
||||
ns,
|
||||
}
|
||||
tmpl.set[name] = tmpl
|
||||
return tmpl
|
||||
}
|
||||
|
||||
// New allocates a new HTML template associated with the given one
|
||||
// and with the same delimiters. The association, which is transitive,
|
||||
// allows one template to invoke another with a {{template}} action.
|
||||
//
|
||||
// If a template with the given name already exists, the new HTML template
|
||||
// will replace it. The existing template will be reset and disassociated with
|
||||
// t.
|
||||
func (t *Template) New(name string) *Template {
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
return t.new(name)
|
||||
}
|
||||
|
||||
// new is the implementation of New, without the lock.
|
||||
func (t *Template) new(name string) *Template {
|
||||
tmpl := &Template{
|
||||
nil,
|
||||
t.text.New(name),
|
||||
nil,
|
||||
t.nameSpace,
|
||||
}
|
||||
if existing, ok := tmpl.set[name]; ok {
|
||||
emptyTmpl := New(existing.Name())
|
||||
*existing = *emptyTmpl
|
||||
}
|
||||
tmpl.set[name] = tmpl
|
||||
return tmpl
|
||||
}
|
||||
|
||||
// Name returns the name of the template.
|
||||
func (t *Template) Name() string {
|
||||
return t.text.Name()
|
||||
}
|
||||
|
||||
// FuncMap is the type of the map defining the mapping from names to
|
||||
// functions. Each function must have either a single return value, or two
|
||||
// return values of which the second has type error. In that case, if the
|
||||
// second (error) argument evaluates to non-nil during execution, execution
|
||||
// terminates and Execute returns that error. FuncMap has the same base type
|
||||
// as FuncMap in "text/template", copied here so clients need not import
|
||||
// "text/template".
|
||||
type FuncMap map[string]interface{}
|
||||
|
||||
// Funcs adds the elements of the argument map to the template's function map.
|
||||
// It must be called before the template is parsed.
|
||||
// It panics if a value in the map is not a function with appropriate return
|
||||
// type. However, it is legal to overwrite elements of the map. The return
|
||||
// value is the template, so calls can be chained.
|
||||
func (t *Template) Funcs(funcMap FuncMap) *Template {
|
||||
t.text.Funcs(template.FuncMap(funcMap))
|
||||
return t
|
||||
}
|
||||
|
||||
// Delims sets the action delimiters to the specified strings, to be used in
|
||||
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
|
||||
// definitions will inherit the settings. An empty delimiter stands for the
|
||||
// corresponding default: {{ or }}.
|
||||
// The return value is the template, so calls can be chained.
|
||||
func (t *Template) Delims(left, right string) *Template {
|
||||
t.text.Delims(left, right)
|
||||
return t
|
||||
}
|
||||
|
||||
// Lookup returns the template with the given name that is associated with t,
|
||||
// or nil if there is no such template.
|
||||
func (t *Template) Lookup(name string) *Template {
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
return t.set[name]
|
||||
}
|
||||
|
||||
// Must is a helper that wraps a call to a function returning (*Template, error)
|
||||
// and panics if the error is non-nil. It is intended for use in variable initializations
|
||||
// such as
|
||||
// var t = template.Must(template.New("name").Parse("html"))
|
||||
func Must(t *Template, err error) *Template {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ParseFiles creates a new Template and parses the template definitions from
|
||||
// the named files. The returned template's name will have the (base) name and
|
||||
// (parsed) contents of the first file. There must be at least one file.
|
||||
// If an error occurs, parsing stops and the returned *Template is nil.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template
|
||||
// named "foo", while "a/foo" is unavailable.
|
||||
func ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(nil, filenames...)
|
||||
}
|
||||
|
||||
// ParseFiles parses the named files and associates the resulting templates with
|
||||
// t. If an error occurs, parsing stops and the returned template is nil;
|
||||
// otherwise it is t. There must be at least one file.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
//
|
||||
// ParseFiles returns an error if t or any associated template has already been executed.
|
||||
func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(t, filenames...)
|
||||
}
|
||||
|
||||
// parseFiles is the helper for the method and function. If the argument
|
||||
// template is nil, it is created from the first file.
|
||||
func parseFiles(t *Template, filenames ...string) (*Template, error) {
|
||||
if err := t.checkCanParse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(filenames) == 0 {
|
||||
// Not really a problem, but be consistent.
|
||||
return nil, fmt.Errorf("html/template: no files named in call to ParseFiles")
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(b)
|
||||
name := filepath.Base(filename)
|
||||
// First template becomes return value if not already defined,
|
||||
// and we use that one for subsequent New calls to associate
|
||||
// all the templates together. Also, if this file has the same name
|
||||
// as t, this file becomes the contents of t, so
|
||||
// t, err := New(name).Funcs(xxx).ParseFiles(name)
|
||||
// works. Otherwise we create a new template associated with t.
|
||||
var tmpl *Template
|
||||
if t == nil {
|
||||
t = New(name)
|
||||
}
|
||||
if name == t.Name() {
|
||||
tmpl = t
|
||||
} else {
|
||||
tmpl = t.New(name)
|
||||
}
|
||||
_, err = tmpl.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ParseGlob creates a new Template and parses the template definitions from
|
||||
// the files identified by the pattern. The files are matched according to the
|
||||
// semantics of filepath.Match, and the pattern must match at least one file.
|
||||
// The returned template will have the (base) name and (parsed) contents of the
|
||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
||||
// ParseFiles with the list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
func ParseGlob(pattern string) (*Template, error) {
|
||||
return parseGlob(nil, pattern)
|
||||
}
|
||||
|
||||
// ParseGlob parses the template definitions in the files identified by the
|
||||
// pattern and associates the resulting templates with t. The files are matched
|
||||
// according to the semantics of filepath.Match, and the pattern must match at
|
||||
// least one file. ParseGlob is equivalent to calling t.ParseFiles with the
|
||||
// list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
//
|
||||
// ParseGlob returns an error if t or any associated template has already been executed.
|
||||
func (t *Template) ParseGlob(pattern string) (*Template, error) {
|
||||
return parseGlob(t, pattern)
|
||||
}
|
||||
|
||||
// parseGlob is the implementation of the function and method ParseGlob.
|
||||
func parseGlob(t *Template, pattern string) (*Template, error) {
|
||||
if err := t.checkCanParse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filenames, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return nil, fmt.Errorf("html/template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
return parseFiles(t, filenames...)
|
||||
}
|
||||
|
||||
// IsTrue reports whether the value is 'true', in the sense of not the zero of its type,
|
||||
// and whether the value has a meaningful truth value. This is the definition of
|
||||
// truth used by if and other such actions.
|
||||
func IsTrue(val interface{}) (truth, ok bool) {
|
||||
return template.IsTrue(val)
|
||||
}
|
166
tpl/internal/go_templates/htmltemplate/template_test.go
Normal file
166
tpl/internal/go_templates/htmltemplate/template_test.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
// 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 go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
)
|
||||
|
||||
func TestTemplateClone(t *testing.T) {
|
||||
// https://golang.org/issue/12996
|
||||
orig := New("name")
|
||||
clone, err := orig.Clone()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(clone.Templates()) != len(orig.Templates()) {
|
||||
t.Fatalf("Invalid length of t.Clone().Templates()")
|
||||
}
|
||||
|
||||
const want = "stuff"
|
||||
parsed := Must(clone.Parse(want))
|
||||
var buf bytes.Buffer
|
||||
err = parsed.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := buf.String(); got != want {
|
||||
t.Fatalf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedefineNonEmptyAfterExecution(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `foo`)
|
||||
c.mustExecute(c.root, nil, "foo")
|
||||
c.mustNotParse(c.root, `bar`)
|
||||
}
|
||||
|
||||
func TestRedefineEmptyAfterExecution(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, ``)
|
||||
c.mustExecute(c.root, nil, "")
|
||||
c.mustNotParse(c.root, `foo`)
|
||||
c.mustExecute(c.root, nil, "")
|
||||
}
|
||||
|
||||
func TestRedefineAfterNonExecution(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `{{if .}}<{{template "X"}}>{{end}}{{define "X"}}foo{{end}}`)
|
||||
c.mustExecute(c.root, 0, "")
|
||||
c.mustNotParse(c.root, `{{define "X"}}bar{{end}}`)
|
||||
c.mustExecute(c.root, 1, "<foo>")
|
||||
}
|
||||
|
||||
func TestRedefineAfterNamedExecution(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `<{{template "X" .}}>{{define "X"}}foo{{end}}`)
|
||||
c.mustExecute(c.root, nil, "<foo>")
|
||||
c.mustNotParse(c.root, `{{define "X"}}bar{{end}}`)
|
||||
c.mustExecute(c.root, nil, "<foo>")
|
||||
}
|
||||
|
||||
func TestRedefineNestedByNameAfterExecution(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `{{define "X"}}foo{{end}}`)
|
||||
c.mustExecute(c.lookup("X"), nil, "foo")
|
||||
c.mustNotParse(c.root, `{{define "X"}}bar{{end}}`)
|
||||
c.mustExecute(c.lookup("X"), nil, "foo")
|
||||
}
|
||||
|
||||
func TestRedefineNestedByTemplateAfterExecution(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `{{define "X"}}foo{{end}}`)
|
||||
c.mustExecute(c.lookup("X"), nil, "foo")
|
||||
c.mustNotParse(c.lookup("X"), `bar`)
|
||||
c.mustExecute(c.lookup("X"), nil, "foo")
|
||||
}
|
||||
|
||||
func TestRedefineSafety(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `<html><a href="{{template "X"}}">{{define "X"}}{{end}}`)
|
||||
c.mustExecute(c.root, nil, `<html><a href="">`)
|
||||
// Note: Every version of Go prior to Go 1.8 accepted the redefinition of "X"
|
||||
// on the next line, but luckily kept it from being used in the outer template.
|
||||
// Now we reject it, which makes clearer that we're not going to use it.
|
||||
c.mustNotParse(c.root, `{{define "X"}}" bar="baz{{end}}`)
|
||||
c.mustExecute(c.root, nil, `<html><a href="">`)
|
||||
}
|
||||
|
||||
func TestRedefineTopUse(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `{{template "X"}}{{.}}{{define "X"}}{{end}}`)
|
||||
c.mustExecute(c.root, 42, `42`)
|
||||
c.mustNotParse(c.root, `{{define "X"}}<script>{{end}}`)
|
||||
c.mustExecute(c.root, 42, `42`)
|
||||
}
|
||||
|
||||
func TestRedefineOtherParsers(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, ``)
|
||||
c.mustExecute(c.root, nil, ``)
|
||||
if _, err := c.root.ParseFiles("no.template"); err == nil || !strings.Contains(err.Error(), "Execute") {
|
||||
t.Errorf("ParseFiles: %v\nwanted error about already having Executed", err)
|
||||
}
|
||||
if _, err := c.root.ParseGlob("*.no.template"); err == nil || !strings.Contains(err.Error(), "Execute") {
|
||||
t.Errorf("ParseGlob: %v\nwanted error about already having Executed", err)
|
||||
}
|
||||
if _, err := c.root.AddParseTree("t1", c.root.Tree); err == nil || !strings.Contains(err.Error(), "Execute") {
|
||||
t.Errorf("AddParseTree: %v\nwanted error about already having Executed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumbers(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
c.mustParse(c.root, `{{print 1_2.3_4}} {{print 0x0_1.e_0p+02}}`)
|
||||
c.mustExecute(c.root, nil, "12.34 7.5")
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
t *testing.T
|
||||
root *Template
|
||||
}
|
||||
|
||||
func newTestCase(t *testing.T) *testCase {
|
||||
return &testCase{
|
||||
t: t,
|
||||
root: New("root"),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCase) lookup(name string) *Template {
|
||||
return c.root.Lookup(name)
|
||||
}
|
||||
|
||||
func (c *testCase) mustParse(t *Template, text string) {
|
||||
_, err := t.Parse(text)
|
||||
if err != nil {
|
||||
c.t.Fatalf("parse: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCase) mustNotParse(t *Template, text string) {
|
||||
_, err := t.Parse(text)
|
||||
if err == nil {
|
||||
c.t.Fatalf("parse: unexpected success")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCase) mustExecute(t *Template, val interface{}, want string) {
|
||||
var buf bytes.Buffer
|
||||
err := t.Execute(&buf, val)
|
||||
if err != nil {
|
||||
c.t.Fatalf("execute: %v", err)
|
||||
}
|
||||
if buf.String() != want {
|
||||
c.t.Fatalf("template output:\n%s\nwant:\n%s", buf.String(), want)
|
||||
}
|
||||
}
|
592
tpl/internal/go_templates/htmltemplate/transition.go
Normal file
592
tpl/internal/go_templates/htmltemplate/transition.go
Normal file
|
@ -0,0 +1,592 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// transitionFunc is the array of context transition functions for text nodes.
|
||||
// A transition function takes a context and template text input, and returns
|
||||
// the updated context and the number of bytes consumed from the front of the
|
||||
// input.
|
||||
var transitionFunc = [...]func(context, []byte) (context, int){
|
||||
stateText: tText,
|
||||
stateTag: tTag,
|
||||
stateAttrName: tAttrName,
|
||||
stateAfterName: tAfterName,
|
||||
stateBeforeValue: tBeforeValue,
|
||||
stateHTMLCmt: tHTMLCmt,
|
||||
stateRCDATA: tSpecialTagEnd,
|
||||
stateAttr: tAttr,
|
||||
stateURL: tURL,
|
||||
stateSrcset: tURL,
|
||||
stateJS: tJS,
|
||||
stateJSDqStr: tJSDelimited,
|
||||
stateJSSqStr: tJSDelimited,
|
||||
stateJSRegexp: tJSDelimited,
|
||||
stateJSBlockCmt: tBlockCmt,
|
||||
stateJSLineCmt: tLineCmt,
|
||||
stateCSS: tCSS,
|
||||
stateCSSDqStr: tCSSStr,
|
||||
stateCSSSqStr: tCSSStr,
|
||||
stateCSSDqURL: tCSSStr,
|
||||
stateCSSSqURL: tCSSStr,
|
||||
stateCSSURL: tCSSStr,
|
||||
stateCSSBlockCmt: tBlockCmt,
|
||||
stateCSSLineCmt: tLineCmt,
|
||||
stateError: tError,
|
||||
}
|
||||
|
||||
var commentStart = []byte("<!--")
|
||||
var commentEnd = []byte("-->")
|
||||
|
||||
// tText is the context transition function for the text state.
|
||||
func tText(c context, s []byte) (context, int) {
|
||||
k := 0
|
||||
for {
|
||||
i := k + bytes.IndexByte(s[k:], '<')
|
||||
if i < k || i+1 == len(s) {
|
||||
return c, len(s)
|
||||
} else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) {
|
||||
return context{state: stateHTMLCmt}, i + 4
|
||||
}
|
||||
i++
|
||||
end := false
|
||||
if s[i] == '/' {
|
||||
if i+1 == len(s) {
|
||||
return c, len(s)
|
||||
}
|
||||
end, i = true, i+1
|
||||
}
|
||||
j, e := eatTagName(s, i)
|
||||
if j != i {
|
||||
if end {
|
||||
e = elementNone
|
||||
}
|
||||
// We've found an HTML tag.
|
||||
return context{state: stateTag, element: e}, j
|
||||
}
|
||||
k = j
|
||||
}
|
||||
}
|
||||
|
||||
var elementContentType = [...]state{
|
||||
elementNone: stateText,
|
||||
elementScript: stateJS,
|
||||
elementStyle: stateCSS,
|
||||
elementTextarea: stateRCDATA,
|
||||
elementTitle: stateRCDATA,
|
||||
}
|
||||
|
||||
// tTag is the context transition function for the tag state.
|
||||
func tTag(c context, s []byte) (context, int) {
|
||||
// Find the attribute name.
|
||||
i := eatWhiteSpace(s, 0)
|
||||
if i == len(s) {
|
||||
return c, len(s)
|
||||
}
|
||||
if s[i] == '>' {
|
||||
return context{
|
||||
state: elementContentType[c.element],
|
||||
element: c.element,
|
||||
}, i + 1
|
||||
}
|
||||
j, err := eatAttrName(s, i)
|
||||
if err != nil {
|
||||
return context{state: stateError, err: err}, len(s)
|
||||
}
|
||||
state, attr := stateTag, attrNone
|
||||
if i == j {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]),
|
||||
}, len(s)
|
||||
}
|
||||
|
||||
attrName := strings.ToLower(string(s[i:j]))
|
||||
if c.element == elementScript && attrName == "type" {
|
||||
attr = attrScriptType
|
||||
} else {
|
||||
switch attrType(attrName) {
|
||||
case contentTypeURL:
|
||||
attr = attrURL
|
||||
case contentTypeCSS:
|
||||
attr = attrStyle
|
||||
case contentTypeJS:
|
||||
attr = attrScript
|
||||
case contentTypeSrcset:
|
||||
attr = attrSrcset
|
||||
}
|
||||
}
|
||||
|
||||
if j == len(s) {
|
||||
state = stateAttrName
|
||||
} else {
|
||||
state = stateAfterName
|
||||
}
|
||||
return context{state: state, element: c.element, attr: attr}, j
|
||||
}
|
||||
|
||||
// tAttrName is the context transition function for stateAttrName.
|
||||
func tAttrName(c context, s []byte) (context, int) {
|
||||
i, err := eatAttrName(s, 0)
|
||||
if err != nil {
|
||||
return context{state: stateError, err: err}, len(s)
|
||||
} else if i != len(s) {
|
||||
c.state = stateAfterName
|
||||
}
|
||||
return c, i
|
||||
}
|
||||
|
||||
// tAfterName is the context transition function for stateAfterName.
|
||||
func tAfterName(c context, s []byte) (context, int) {
|
||||
// Look for the start of the value.
|
||||
i := eatWhiteSpace(s, 0)
|
||||
if i == len(s) {
|
||||
return c, len(s)
|
||||
} else if s[i] != '=' {
|
||||
// Occurs due to tag ending '>', and valueless attribute.
|
||||
c.state = stateTag
|
||||
return c, i
|
||||
}
|
||||
c.state = stateBeforeValue
|
||||
// Consume the "=".
|
||||
return c, i + 1
|
||||
}
|
||||
|
||||
var attrStartStates = [...]state{
|
||||
attrNone: stateAttr,
|
||||
attrScript: stateJS,
|
||||
attrScriptType: stateAttr,
|
||||
attrStyle: stateCSS,
|
||||
attrURL: stateURL,
|
||||
attrSrcset: stateSrcset,
|
||||
}
|
||||
|
||||
// tBeforeValue is the context transition function for stateBeforeValue.
|
||||
func tBeforeValue(c context, s []byte) (context, int) {
|
||||
i := eatWhiteSpace(s, 0)
|
||||
if i == len(s) {
|
||||
return c, len(s)
|
||||
}
|
||||
// Find the attribute delimiter.
|
||||
delim := delimSpaceOrTagEnd
|
||||
switch s[i] {
|
||||
case '\'':
|
||||
delim, i = delimSingleQuote, i+1
|
||||
case '"':
|
||||
delim, i = delimDoubleQuote, i+1
|
||||
}
|
||||
c.state, c.delim = attrStartStates[c.attr], delim
|
||||
return c, i
|
||||
}
|
||||
|
||||
// tHTMLCmt is the context transition function for stateHTMLCmt.
|
||||
func tHTMLCmt(c context, s []byte) (context, int) {
|
||||
if i := bytes.Index(s, commentEnd); i != -1 {
|
||||
return context{}, i + 3
|
||||
}
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
// specialTagEndMarkers maps element types to the character sequence that
|
||||
// case-insensitively signals the end of the special tag body.
|
||||
var specialTagEndMarkers = [...][]byte{
|
||||
elementScript: []byte("script"),
|
||||
elementStyle: []byte("style"),
|
||||
elementTextarea: []byte("textarea"),
|
||||
elementTitle: []byte("title"),
|
||||
}
|
||||
|
||||
var (
|
||||
specialTagEndPrefix = []byte("</")
|
||||
tagEndSeparators = []byte("> \t\n\f/")
|
||||
)
|
||||
|
||||
// tSpecialTagEnd is the context transition function for raw text and RCDATA
|
||||
// element states.
|
||||
func tSpecialTagEnd(c context, s []byte) (context, int) {
|
||||
if c.element != elementNone {
|
||||
if i := indexTagEnd(s, specialTagEndMarkers[c.element]); i != -1 {
|
||||
return context{}, i
|
||||
}
|
||||
}
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
// indexTagEnd finds the index of a special tag end in a case insensitive way, or returns -1
|
||||
func indexTagEnd(s []byte, tag []byte) int {
|
||||
res := 0
|
||||
plen := len(specialTagEndPrefix)
|
||||
for len(s) > 0 {
|
||||
// Try to find the tag end prefix first
|
||||
i := bytes.Index(s, specialTagEndPrefix)
|
||||
if i == -1 {
|
||||
return i
|
||||
}
|
||||
s = s[i+plen:]
|
||||
// Try to match the actual tag if there is still space for it
|
||||
if len(tag) <= len(s) && bytes.EqualFold(tag, s[:len(tag)]) {
|
||||
s = s[len(tag):]
|
||||
// Check the tag is followed by a proper separator
|
||||
if len(s) > 0 && bytes.IndexByte(tagEndSeparators, s[0]) != -1 {
|
||||
return res + i
|
||||
}
|
||||
res += len(tag)
|
||||
}
|
||||
res += i + plen
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// tAttr is the context transition function for the attribute state.
|
||||
func tAttr(c context, s []byte) (context, int) {
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
// tURL is the context transition function for the URL state.
|
||||
func tURL(c context, s []byte) (context, int) {
|
||||
if bytes.ContainsAny(s, "#?") {
|
||||
c.urlPart = urlPartQueryOrFrag
|
||||
} else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone {
|
||||
// HTML5 uses "Valid URL potentially surrounded by spaces" for
|
||||
// attrs: https://www.w3.org/TR/html5/index.html#attributes-1
|
||||
c.urlPart = urlPartPreQuery
|
||||
}
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
// tJS is the context transition function for the JS state.
|
||||
func tJS(c context, s []byte) (context, int) {
|
||||
i := bytes.IndexAny(s, `"'/`)
|
||||
if i == -1 {
|
||||
// Entire input is non string, comment, regexp tokens.
|
||||
c.jsCtx = nextJSCtx(s, c.jsCtx)
|
||||
return c, len(s)
|
||||
}
|
||||
c.jsCtx = nextJSCtx(s[:i], c.jsCtx)
|
||||
switch s[i] {
|
||||
case '"':
|
||||
c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
|
||||
case '\'':
|
||||
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
|
||||
case '/':
|
||||
switch {
|
||||
case i+1 < len(s) && s[i+1] == '/':
|
||||
c.state, i = stateJSLineCmt, i+1
|
||||
case i+1 < len(s) && s[i+1] == '*':
|
||||
c.state, i = stateJSBlockCmt, i+1
|
||||
case c.jsCtx == jsCtxRegexp:
|
||||
c.state = stateJSRegexp
|
||||
case c.jsCtx == jsCtxDivOp:
|
||||
c.jsCtx = jsCtxRegexp
|
||||
default:
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]),
|
||||
}, len(s)
|
||||
}
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
return c, i + 1
|
||||
}
|
||||
|
||||
// tJSDelimited is the context transition function for the JS string and regexp
|
||||
// states.
|
||||
func tJSDelimited(c context, s []byte) (context, int) {
|
||||
specials := `\"`
|
||||
switch c.state {
|
||||
case stateJSSqStr:
|
||||
specials = `\'`
|
||||
case stateJSRegexp:
|
||||
specials = `\/[]`
|
||||
}
|
||||
|
||||
k, inCharset := 0, false
|
||||
for {
|
||||
i := k + bytes.IndexAny(s[k:], specials)
|
||||
if i < k {
|
||||
break
|
||||
}
|
||||
switch s[i] {
|
||||
case '\\':
|
||||
i++
|
||||
if i == len(s) {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
|
||||
}, len(s)
|
||||
}
|
||||
case '[':
|
||||
inCharset = true
|
||||
case ']':
|
||||
inCharset = false
|
||||
default:
|
||||
// end delimiter
|
||||
if !inCharset {
|
||||
c.state, c.jsCtx = stateJS, jsCtxDivOp
|
||||
return c, i + 1
|
||||
}
|
||||
}
|
||||
k = i + 1
|
||||
}
|
||||
|
||||
if inCharset {
|
||||
// This can be fixed by making context richer if interpolation
|
||||
// into charsets is desired.
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrPartialCharset, nil, 0, "unfinished JS regexp charset: %q", s),
|
||||
}, len(s)
|
||||
}
|
||||
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
var blockCommentEnd = []byte("*/")
|
||||
|
||||
// tBlockCmt is the context transition function for /*comment*/ states.
|
||||
func tBlockCmt(c context, s []byte) (context, int) {
|
||||
i := bytes.Index(s, blockCommentEnd)
|
||||
if i == -1 {
|
||||
return c, len(s)
|
||||
}
|
||||
switch c.state {
|
||||
case stateJSBlockCmt:
|
||||
c.state = stateJS
|
||||
case stateCSSBlockCmt:
|
||||
c.state = stateCSS
|
||||
default:
|
||||
panic(c.state.String())
|
||||
}
|
||||
return c, i + 2
|
||||
}
|
||||
|
||||
// tLineCmt is the context transition function for //comment states.
|
||||
func tLineCmt(c context, s []byte) (context, int) {
|
||||
var lineTerminators string
|
||||
var endState state
|
||||
switch c.state {
|
||||
case stateJSLineCmt:
|
||||
lineTerminators, endState = "\n\r\u2028\u2029", stateJS
|
||||
case stateCSSLineCmt:
|
||||
lineTerminators, endState = "\n\f\r", stateCSS
|
||||
// Line comments are not part of any published CSS standard but
|
||||
// are supported by the 4 major browsers.
|
||||
// This defines line comments as
|
||||
// LINECOMMENT ::= "//" [^\n\f\d]*
|
||||
// since https://www.w3.org/TR/css3-syntax/#SUBTOK-nl defines
|
||||
// newlines:
|
||||
// nl ::= #xA | #xD #xA | #xD | #xC
|
||||
default:
|
||||
panic(c.state.String())
|
||||
}
|
||||
|
||||
i := bytes.IndexAny(s, lineTerminators)
|
||||
if i == -1 {
|
||||
return c, len(s)
|
||||
}
|
||||
c.state = endState
|
||||
// Per section 7.4 of EcmaScript 5 : https://es5.github.com/#x7.4
|
||||
// "However, the LineTerminator at the end of the line is not
|
||||
// considered to be part of the single-line comment; it is
|
||||
// recognized separately by the lexical grammar and becomes part
|
||||
// of the stream of input elements for the syntactic grammar."
|
||||
return c, i
|
||||
}
|
||||
|
||||
// tCSS is the context transition function for the CSS state.
|
||||
func tCSS(c context, s []byte) (context, int) {
|
||||
// CSS quoted strings are almost never used except for:
|
||||
// (1) URLs as in background: "/foo.png"
|
||||
// (2) Multiword font-names as in font-family: "Times New Roman"
|
||||
// (3) List separators in content values as in inline-lists:
|
||||
// <style>
|
||||
// ul.inlineList { list-style: none; padding:0 }
|
||||
// ul.inlineList > li { display: inline }
|
||||
// ul.inlineList > li:before { content: ", " }
|
||||
// ul.inlineList > li:first-child:before { content: "" }
|
||||
// </style>
|
||||
// <ul class=inlineList><li>One<li>Two<li>Three</ul>
|
||||
// (4) Attribute value selectors as in a[href="http://example.com/"]
|
||||
//
|
||||
// We conservatively treat all strings as URLs, but make some
|
||||
// allowances to avoid confusion.
|
||||
//
|
||||
// In (1), our conservative assumption is justified.
|
||||
// In (2), valid font names do not contain ':', '?', or '#', so our
|
||||
// conservative assumption is fine since we will never transition past
|
||||
// urlPartPreQuery.
|
||||
// In (3), our protocol heuristic should not be tripped, and there
|
||||
// should not be non-space content after a '?' or '#', so as long as
|
||||
// we only %-encode RFC 3986 reserved characters we are ok.
|
||||
// In (4), we should URL escape for URL attributes, and for others we
|
||||
// have the attribute name available if our conservative assumption
|
||||
// proves problematic for real code.
|
||||
|
||||
k := 0
|
||||
for {
|
||||
i := k + bytes.IndexAny(s[k:], `("'/`)
|
||||
if i < k {
|
||||
return c, len(s)
|
||||
}
|
||||
switch s[i] {
|
||||
case '(':
|
||||
// Look for url to the left.
|
||||
p := bytes.TrimRight(s[:i], "\t\n\f\r ")
|
||||
if endsWithCSSKeyword(p, "url") {
|
||||
j := len(s) - len(bytes.TrimLeft(s[i+1:], "\t\n\f\r "))
|
||||
switch {
|
||||
case j != len(s) && s[j] == '"':
|
||||
c.state, j = stateCSSDqURL, j+1
|
||||
case j != len(s) && s[j] == '\'':
|
||||
c.state, j = stateCSSSqURL, j+1
|
||||
default:
|
||||
c.state = stateCSSURL
|
||||
}
|
||||
return c, j
|
||||
}
|
||||
case '/':
|
||||
if i+1 < len(s) {
|
||||
switch s[i+1] {
|
||||
case '/':
|
||||
c.state = stateCSSLineCmt
|
||||
return c, i + 2
|
||||
case '*':
|
||||
c.state = stateCSSBlockCmt
|
||||
return c, i + 2
|
||||
}
|
||||
}
|
||||
case '"':
|
||||
c.state = stateCSSDqStr
|
||||
return c, i + 1
|
||||
case '\'':
|
||||
c.state = stateCSSSqStr
|
||||
return c, i + 1
|
||||
}
|
||||
k = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// tCSSStr is the context transition function for the CSS string and URL states.
|
||||
func tCSSStr(c context, s []byte) (context, int) {
|
||||
var endAndEsc string
|
||||
switch c.state {
|
||||
case stateCSSDqStr, stateCSSDqURL:
|
||||
endAndEsc = `\"`
|
||||
case stateCSSSqStr, stateCSSSqURL:
|
||||
endAndEsc = `\'`
|
||||
case stateCSSURL:
|
||||
// Unquoted URLs end with a newline or close parenthesis.
|
||||
// The below includes the wc (whitespace character) and nl.
|
||||
endAndEsc = "\\\t\n\f\r )"
|
||||
default:
|
||||
panic(c.state.String())
|
||||
}
|
||||
|
||||
k := 0
|
||||
for {
|
||||
i := k + bytes.IndexAny(s[k:], endAndEsc)
|
||||
if i < k {
|
||||
c, nread := tURL(c, decodeCSS(s[k:]))
|
||||
return c, k + nread
|
||||
}
|
||||
if s[i] == '\\' {
|
||||
i++
|
||||
if i == len(s) {
|
||||
return context{
|
||||
state: stateError,
|
||||
err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in CSS string: %q", s),
|
||||
}, len(s)
|
||||
}
|
||||
} else {
|
||||
c.state = stateCSS
|
||||
return c, i + 1
|
||||
}
|
||||
c, _ = tURL(c, decodeCSS(s[:i+1]))
|
||||
k = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// tError is the context transition function for the error state.
|
||||
func tError(c context, s []byte) (context, int) {
|
||||
return c, len(s)
|
||||
}
|
||||
|
||||
// eatAttrName returns the largest j such that s[i:j] is an attribute name.
|
||||
// It returns an error if s[i:] does not look like it begins with an
|
||||
// attribute name, such as encountering a quote mark without a preceding
|
||||
// equals sign.
|
||||
func eatAttrName(s []byte, i int) (int, *Error) {
|
||||
for j := i; j < len(s); j++ {
|
||||
switch s[j] {
|
||||
case ' ', '\t', '\n', '\f', '\r', '=', '>':
|
||||
return j, nil
|
||||
case '\'', '"', '<':
|
||||
// These result in a parse warning in HTML5 and are
|
||||
// indicative of serious problems if seen in an attr
|
||||
// name in a template.
|
||||
return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s)
|
||||
default:
|
||||
// No-op.
|
||||
}
|
||||
}
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
var elementNameMap = map[string]element{
|
||||
"script": elementScript,
|
||||
"style": elementStyle,
|
||||
"textarea": elementTextarea,
|
||||
"title": elementTitle,
|
||||
}
|
||||
|
||||
// asciiAlpha reports whether c is an ASCII letter.
|
||||
func asciiAlpha(c byte) bool {
|
||||
return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
|
||||
}
|
||||
|
||||
// asciiAlphaNum reports whether c is an ASCII letter or digit.
|
||||
func asciiAlphaNum(c byte) bool {
|
||||
return asciiAlpha(c) || '0' <= c && c <= '9'
|
||||
}
|
||||
|
||||
// eatTagName returns the largest j such that s[i:j] is a tag name and the tag type.
|
||||
func eatTagName(s []byte, i int) (int, element) {
|
||||
if i == len(s) || !asciiAlpha(s[i]) {
|
||||
return i, elementNone
|
||||
}
|
||||
j := i + 1
|
||||
for j < len(s) {
|
||||
x := s[j]
|
||||
if asciiAlphaNum(x) {
|
||||
j++
|
||||
continue
|
||||
}
|
||||
// Allow "x-y" or "x:y" but not "x-", "-y", or "x--y".
|
||||
if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) {
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return j, elementNameMap[strings.ToLower(string(s[i:j]))]
|
||||
}
|
||||
|
||||
// eatWhiteSpace returns the largest j such that s[i:j] is white space.
|
||||
func eatWhiteSpace(s []byte, i int) int {
|
||||
for j := i; j < len(s); j++ {
|
||||
switch s[j] {
|
||||
case ' ', '\t', '\n', '\f', '\r':
|
||||
// No-op.
|
||||
default:
|
||||
return j
|
||||
}
|
||||
}
|
||||
return len(s)
|
||||
}
|
62
tpl/internal/go_templates/htmltemplate/transition_test.go
Normal file
62
tpl/internal/go_templates/htmltemplate/transition_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindEndTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
s, tag string
|
||||
want int
|
||||
}{
|
||||
{"", "tag", -1},
|
||||
{"hello </textarea> hello", "textarea", 6},
|
||||
{"hello </TEXTarea> hello", "textarea", 6},
|
||||
{"hello </textAREA>", "textarea", 6},
|
||||
{"hello </textarea", "textareax", -1},
|
||||
{"hello </textarea>", "tag", -1},
|
||||
{"hello tag </textarea", "tag", -1},
|
||||
{"hello </tag> </other> </textarea> <other>", "textarea", 22},
|
||||
{"</textarea> <other>", "textarea", 0},
|
||||
{"<div> </div> </TEXTAREA>", "textarea", 13},
|
||||
{"<div> </div> </TEXTAREA\t>", "textarea", 13},
|
||||
{"<div> </div> </TEXTAREA >", "textarea", 13},
|
||||
{"<div> </div> </TEXTAREAfoo", "textarea", -1},
|
||||
{"</TEXTAREAfoo </textarea>", "textarea", 14},
|
||||
{"<</script >", "script", 1},
|
||||
{"</script>", "textarea", -1},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := indexTagEnd([]byte(test.s), []byte(test.tag)); test.want != got {
|
||||
t.Errorf("%q/%q: want\n\t%d\nbut got\n\t%d", test.s, test.tag, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTemplateSpecialTags(b *testing.B) {
|
||||
|
||||
r := struct {
|
||||
Name, Gift string
|
||||
}{"Aunt Mildred", "bone china tea set"}
|
||||
|
||||
h1 := "<textarea> Hello Hello Hello </textarea> "
|
||||
h2 := "<textarea> <p> Dear {{.Name}},\n{{with .Gift}}Thank you for the lovely {{.}}. {{end}}\nBest wishes. </p>\n</textarea>"
|
||||
html := strings.Repeat(h1, 100) + h2 + strings.Repeat(h1, 100) + h2
|
||||
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < b.N; i++ {
|
||||
tmpl := Must(New("foo").Parse(html))
|
||||
if err := tmpl.Execute(&buf, r); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
219
tpl/internal/go_templates/htmltemplate/url.go
Normal file
219
tpl/internal/go_templates/htmltemplate/url.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// urlFilter returns its input unless it contains an unsafe scheme in which
|
||||
// case it defangs the entire URL.
|
||||
//
|
||||
// Schemes that cause unintended side effects that are irreversible without user
|
||||
// interaction are considered unsafe. For example, clicking on a "javascript:"
|
||||
// link can immediately trigger JavaScript code execution.
|
||||
//
|
||||
// This filter conservatively assumes that all schemes other than the following
|
||||
// are unsafe:
|
||||
// * http: Navigates to a new website, and may open a new window or tab.
|
||||
// These side effects can be reversed by navigating back to the
|
||||
// previous website, or closing the window or tab. No irreversible
|
||||
// changes will take place without further user interaction with
|
||||
// the new website.
|
||||
// * https: Same as http.
|
||||
// * mailto: Opens an email program and starts a new draft. This side effect
|
||||
// is not irreversible until the user explicitly clicks send; it
|
||||
// can be undone by closing the email program.
|
||||
//
|
||||
// To allow URLs containing other schemes to bypass this filter, developers must
|
||||
// explicitly indicate that such a URL is expected and safe by encapsulating it
|
||||
// in a template.URL value.
|
||||
func urlFilter(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeURL {
|
||||
return s
|
||||
}
|
||||
if !isSafeURL(s) {
|
||||
return "#" + filterFailsafe
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// isSafeURL is true if s is a relative URL or if URL has a protocol in
|
||||
// (http, https, mailto).
|
||||
func isSafeURL(s string) bool {
|
||||
if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') {
|
||||
|
||||
protocol := s[:i]
|
||||
if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// urlEscaper produces an output that can be embedded in a URL query.
|
||||
// The output can be embedded in an HTML attribute without further escaping.
|
||||
func urlEscaper(args ...interface{}) string {
|
||||
return urlProcessor(false, args...)
|
||||
}
|
||||
|
||||
// urlNormalizer normalizes URL content so it can be embedded in a quote-delimited
|
||||
// string or parenthesis delimited url(...).
|
||||
// The normalizer does not encode all HTML specials. Specifically, it does not
|
||||
// encode '&' so correct embedding in an HTML attribute requires escaping of
|
||||
// '&' to '&'.
|
||||
func urlNormalizer(args ...interface{}) string {
|
||||
return urlProcessor(true, args...)
|
||||
}
|
||||
|
||||
// urlProcessor normalizes (when norm is true) or escapes its input to produce
|
||||
// a valid hierarchical or opaque URL part.
|
||||
func urlProcessor(norm bool, args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
if t == contentTypeURL {
|
||||
norm = true
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if processURLOnto(s, norm, &b) {
|
||||
return b.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// processURLOnto appends a normalized URL corresponding to its input to b
|
||||
// and reports whether the appended content differs from s.
|
||||
func processURLOnto(s string, norm bool, b *bytes.Buffer) bool {
|
||||
b.Grow(len(s) + 16)
|
||||
written := 0
|
||||
// The byte loop below assumes that all URLs use UTF-8 as the
|
||||
// content-encoding. This is similar to the URI to IRI encoding scheme
|
||||
// defined in section 3.1 of RFC 3987, and behaves the same as the
|
||||
// EcmaScript builtin encodeURIComponent.
|
||||
// It should not cause any misencoding of URLs in pages with
|
||||
// Content-type: text/html;charset=UTF-8.
|
||||
for i, n := 0, len(s); i < n; i++ {
|
||||
c := s[i]
|
||||
switch c {
|
||||
// Single quote and parens are sub-delims in RFC 3986, but we
|
||||
// escape them so the output can be embedded in single
|
||||
// quoted attributes and unquoted CSS url(...) constructs.
|
||||
// Single quotes are reserved in URLs, but are only used in
|
||||
// the obsolete "mark" rule in an appendix in RFC 3986
|
||||
// so can be safely encoded.
|
||||
case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']':
|
||||
if norm {
|
||||
continue
|
||||
}
|
||||
// Unreserved according to RFC 3986 sec 2.3
|
||||
// "For consistency, percent-encoded octets in the ranges of
|
||||
// ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D),
|
||||
// period (%2E), underscore (%5F), or tilde (%7E) should not be
|
||||
// created by URI producers
|
||||
case '-', '.', '_', '~':
|
||||
continue
|
||||
case '%':
|
||||
// When normalizing do not re-encode valid escapes.
|
||||
if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
// Unreserved according to RFC 3986 sec 2.3
|
||||
if 'a' <= c && c <= 'z' {
|
||||
continue
|
||||
}
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
continue
|
||||
}
|
||||
if '0' <= c && c <= '9' {
|
||||
continue
|
||||
}
|
||||
}
|
||||
b.WriteString(s[written:i])
|
||||
fmt.Fprintf(b, "%%%02x", c)
|
||||
written = i + 1
|
||||
}
|
||||
b.WriteString(s[written:])
|
||||
return written != 0
|
||||
}
|
||||
|
||||
// Filters and normalizes srcset values which are comma separated
|
||||
// URLs followed by metadata.
|
||||
func srcsetFilterAndEscaper(args ...interface{}) string {
|
||||
s, t := stringify(args...)
|
||||
switch t {
|
||||
case contentTypeSrcset:
|
||||
return s
|
||||
case contentTypeURL:
|
||||
// Normalizing gets rid of all HTML whitespace
|
||||
// which separate the image URL from its metadata.
|
||||
var b bytes.Buffer
|
||||
if processURLOnto(s, true, &b) {
|
||||
s = b.String()
|
||||
}
|
||||
// Additionally, commas separate one source from another.
|
||||
return strings.ReplaceAll(s, ",", "%2c")
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
written := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == ',' {
|
||||
filterSrcsetElement(s, written, i, &b)
|
||||
b.WriteString(",")
|
||||
written = i + 1
|
||||
}
|
||||
}
|
||||
filterSrcsetElement(s, written, len(s), &b)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Derived from https://play.golang.org/p/Dhmj7FORT5
|
||||
const htmlSpaceAndASCIIAlnumBytes = "\x00\x36\x00\x00\x01\x00\xff\x03\xfe\xff\xff\x07\xfe\xff\xff\x07"
|
||||
|
||||
// isHTMLSpace is true iff c is a whitespace character per
|
||||
// https://infra.spec.whatwg.org/#ascii-whitespace
|
||||
func isHTMLSpace(c byte) bool {
|
||||
return (c <= 0x20) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
|
||||
}
|
||||
|
||||
func isHTMLSpaceOrASCIIAlnum(c byte) bool {
|
||||
return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
|
||||
}
|
||||
|
||||
func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) {
|
||||
start := left
|
||||
for start < right && isHTMLSpace(s[start]) {
|
||||
start++
|
||||
}
|
||||
end := right
|
||||
for i := start; i < right; i++ {
|
||||
if isHTMLSpace(s[i]) {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if url := s[start:end]; isSafeURL(url) {
|
||||
// If image metadata is only spaces or alnums then
|
||||
// we don't need to URL normalize it.
|
||||
metadataOk := true
|
||||
for i := end; i < right; i++ {
|
||||
if !isHTMLSpaceOrASCIIAlnum(s[i]) {
|
||||
metadataOk = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if metadataOk {
|
||||
b.WriteString(s[left:start])
|
||||
processURLOnto(url, true, b)
|
||||
b.WriteString(s[end:right])
|
||||
return
|
||||
}
|
||||
}
|
||||
b.WriteString("#")
|
||||
b.WriteString(filterFailsafe)
|
||||
}
|
171
tpl/internal/go_templates/htmltemplate/url_test.go
Normal file
171
tpl/internal/go_templates/htmltemplate/url_test.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestURLNormalizer(t *testing.T) {
|
||||
tests := []struct {
|
||||
url, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{
|
||||
"http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag",
|
||||
"http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag",
|
||||
},
|
||||
{" ", "%20"},
|
||||
{"%7c", "%7c"},
|
||||
{"%7C", "%7C"},
|
||||
{"%2", "%252"},
|
||||
{"%", "%25"},
|
||||
{"%z", "%25z"},
|
||||
{"/foo|bar/%5c\u1234", "/foo%7cbar/%5c%e1%88%b4"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := urlNormalizer(test.url); test.want != got {
|
||||
t.Errorf("%q: want\n\t%q\nbut got\n\t%q", test.url, test.want, got)
|
||||
}
|
||||
if test.want != urlNormalizer(test.want) {
|
||||
t.Errorf("not idempotent: %q", test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLFilters(t *testing.T) {
|
||||
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
|
||||
` !"#$%&'()*+,-./` +
|
||||
`0123456789:;<=>?` +
|
||||
`@ABCDEFGHIJKLMNO` +
|
||||
`PQRSTUVWXYZ[\]^_` +
|
||||
"`abcdefghijklmno" +
|
||||
"pqrstuvwxyz{|}~\x7f" +
|
||||
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
escaper func(...interface{}) string
|
||||
escaped string
|
||||
}{
|
||||
{
|
||||
"urlEscaper",
|
||||
urlEscaper,
|
||||
"%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" +
|
||||
"%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" +
|
||||
"%20%21%22%23%24%25%26%27%28%29%2a%2b%2c-.%2f" +
|
||||
"0123456789%3a%3b%3c%3d%3e%3f" +
|
||||
"%40ABCDEFGHIJKLMNO" +
|
||||
"PQRSTUVWXYZ%5b%5c%5d%5e_" +
|
||||
"%60abcdefghijklmno" +
|
||||
"pqrstuvwxyz%7b%7c%7d~%7f" +
|
||||
"%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e",
|
||||
},
|
||||
{
|
||||
"urlNormalizer",
|
||||
urlNormalizer,
|
||||
"%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" +
|
||||
"%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" +
|
||||
"%20!%22#$%25&%27%28%29*+,-./" +
|
||||
"0123456789:;%3c=%3e?" +
|
||||
"@ABCDEFGHIJKLMNO" +
|
||||
"PQRSTUVWXYZ[%5c]%5e_" +
|
||||
"%60abcdefghijklmno" +
|
||||
"pqrstuvwxyz%7b%7c%7d~%7f" +
|
||||
"%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if s := test.escaper(input); s != test.escaped {
|
||||
t.Errorf("%s: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcsetFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"one ok",
|
||||
"http://example.com/img.png",
|
||||
"http://example.com/img.png",
|
||||
},
|
||||
{
|
||||
"one ok with metadata",
|
||||
" /img.png 200w",
|
||||
" /img.png 200w",
|
||||
},
|
||||
{
|
||||
"one bad",
|
||||
"javascript:alert(1) 200w",
|
||||
"#ZgotmplZ",
|
||||
},
|
||||
{
|
||||
"two ok",
|
||||
"foo.png, bar.png",
|
||||
"foo.png, bar.png",
|
||||
},
|
||||
{
|
||||
"left bad",
|
||||
"javascript:alert(1), /foo.png",
|
||||
"#ZgotmplZ, /foo.png",
|
||||
},
|
||||
{
|
||||
"right bad",
|
||||
"/bogus#, javascript:alert(1)",
|
||||
"/bogus#,#ZgotmplZ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := srcsetFilterAndEscaper(test.input); got != test.want {
|
||||
t.Errorf("%s: srcsetFilterAndEscaper(%q) want %q != %q", test.name, test.input, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURLEscaper(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
urlEscaper("http://example.com:80/foo?q=bar%20&baz=x+y#frag")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURLEscaperNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
urlEscaper("TheQuickBrownFoxJumpsOverTheLazyDog.")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURLNormalizer(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
urlNormalizer("The quick brown fox jumps over the lazy dog.\n")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURLNormalizerNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
urlNormalizer("http://example.com:80/foo?q=bar%20&baz=x+y#frag")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSrcsetFilter(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
srcsetFilterAndEscaper(" /foo/bar.png 200w, /baz/boo(1).png")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSrcsetFilterNoSpecials(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
srcsetFilterAndEscaper("http://example.com:80/foo?q=bar%20&baz=x+y#frag")
|
||||
}
|
||||
}
|
16
tpl/internal/go_templates/htmltemplate/urlpart_string.go
Normal file
16
tpl/internal/go_templates/htmltemplate/urlpart_string.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type urlPart"; DO NOT EDIT.
|
||||
|
||||
package template
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _urlPart_name = "urlPartNoneurlPartPreQueryurlPartQueryOrFragurlPartUnknown"
|
||||
|
||||
var _urlPart_index = [...]uint8{0, 11, 26, 44, 58}
|
||||
|
||||
func (i urlPart) String() string {
|
||||
if i >= urlPart(len(_urlPart_index)-1) {
|
||||
return "urlPart(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _urlPart_name[_urlPart_index[i]:_urlPart_index[i+1]]
|
||||
}
|
456
tpl/internal/go_templates/texttemplate/doc.go
Normal file
456
tpl/internal/go_templates/texttemplate/doc.go
Normal file
|
@ -0,0 +1,456 @@
|
|||
// Copyright 2011 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 template implements data-driven templates for generating textual output.
|
||||
|
||||
To generate HTML output, see package html/template, which has the same interface
|
||||
as this package but automatically secures HTML output against certain attacks.
|
||||
|
||||
Templates are executed by applying them to a data structure. Annotations in the
|
||||
template refer to elements of the data structure (typically a field of a struct
|
||||
or a key in a map) to control execution and derive values to be displayed.
|
||||
Execution of the template walks the structure and sets the cursor, represented
|
||||
by a period '.' and called "dot", to the value at the current location in the
|
||||
structure as execution proceeds.
|
||||
|
||||
The input text for a template is UTF-8-encoded text in any format.
|
||||
"Actions"--data evaluations or control structures--are delimited by
|
||||
"{{" and "}}"; all text outside actions is copied to the output unchanged.
|
||||
Except for raw strings, actions may not span newlines, although comments can.
|
||||
|
||||
Once parsed, a template may be executed safely in parallel, although if parallel
|
||||
executions share a Writer the output may be interleaved.
|
||||
|
||||
Here is a trivial example that prints "17 items are made of wool".
|
||||
|
||||
type Inventory struct {
|
||||
Material string
|
||||
Count uint
|
||||
}
|
||||
sweaters := Inventory{"wool", 17}
|
||||
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
|
||||
if err != nil { panic(err) }
|
||||
err = tmpl.Execute(os.Stdout, sweaters)
|
||||
if err != nil { panic(err) }
|
||||
|
||||
More intricate examples appear below.
|
||||
|
||||
Text and spaces
|
||||
|
||||
By default, all text between actions is copied verbatim when the template is
|
||||
executed. For example, the string " items are made of " in the example above appears
|
||||
on standard output when the program is run.
|
||||
|
||||
However, to aid in formatting template source code, if an action's left delimiter
|
||||
(by default "{{") is followed immediately by a minus sign and ASCII space character
|
||||
("{{- "), all trailing white space is trimmed from the immediately preceding text.
|
||||
Similarly, if the right delimiter ("}}") is preceded by a space and minus sign
|
||||
(" -}}"), all leading white space is trimmed from the immediately following text.
|
||||
In these trim markers, the ASCII space must be present; "{{-3}}" parses as an
|
||||
action containing the number -3.
|
||||
|
||||
For instance, when executing the template whose source is
|
||||
|
||||
"{{23 -}} < {{- 45}}"
|
||||
|
||||
the generated output would be
|
||||
|
||||
"23<45"
|
||||
|
||||
For this trimming, the definition of white space characters is the same as in Go:
|
||||
space, horizontal tab, carriage return, and newline.
|
||||
|
||||
Actions
|
||||
|
||||
Here is the list of actions. "Arguments" and "pipelines" are evaluations of
|
||||
data, defined in detail in the corresponding sections that follow.
|
||||
|
||||
*/
|
||||
// {{/* a comment */}}
|
||||
// {{- /* a comment with white space trimmed from preceding and following text */ -}}
|
||||
// A comment; discarded. May contain newlines.
|
||||
// Comments do not nest and must start and end at the
|
||||
// delimiters, as shown here.
|
||||
/*
|
||||
|
||||
{{pipeline}}
|
||||
The default textual representation (the same as would be
|
||||
printed by fmt.Print) of the value of the pipeline is copied
|
||||
to the output.
|
||||
|
||||
{{if pipeline}} T1 {{end}}
|
||||
If the value of the pipeline is empty, no output is generated;
|
||||
otherwise, T1 is executed. The empty values are false, 0, any
|
||||
nil pointer or interface value, and any array, slice, map, or
|
||||
string of length zero.
|
||||
Dot is unaffected.
|
||||
|
||||
{{if pipeline}} T1 {{else}} T0 {{end}}
|
||||
If the value of the pipeline is empty, T0 is executed;
|
||||
otherwise, T1 is executed. Dot is unaffected.
|
||||
|
||||
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
|
||||
To simplify the appearance of if-else chains, the else action
|
||||
of an if may include another if directly; the effect is exactly
|
||||
the same as writing
|
||||
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
|
||||
|
||||
{{range pipeline}} T1 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
If the value of the pipeline has length zero, nothing is output;
|
||||
otherwise, dot is set to the successive elements of the array,
|
||||
slice, or map and T1 is executed. If the value is a map and the
|
||||
keys are of basic type with a defined order ("comparable"), the
|
||||
elements will be visited in sorted key order.
|
||||
|
||||
{{range pipeline}} T1 {{else}} T0 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
If the value of the pipeline has length zero, dot is unaffected and
|
||||
T0 is executed; otherwise, dot is set to the successive elements
|
||||
of the array, slice, or map and T1 is executed.
|
||||
|
||||
{{template "name"}}
|
||||
The template with the specified name is executed with nil data.
|
||||
|
||||
{{template "name" pipeline}}
|
||||
The template with the specified name is executed with dot set
|
||||
to the value of the pipeline.
|
||||
|
||||
{{block "name" pipeline}} T1 {{end}}
|
||||
A block is shorthand for defining a template
|
||||
{{define "name"}} T1 {{end}}
|
||||
and then executing it in place
|
||||
{{template "name" pipeline}}
|
||||
The typical use is to define a set of root templates that are
|
||||
then customized by redefining the block templates within.
|
||||
|
||||
{{with pipeline}} T1 {{end}}
|
||||
If the value of the pipeline is empty, no output is generated;
|
||||
otherwise, dot is set to the value of the pipeline and T1 is
|
||||
executed.
|
||||
|
||||
{{with pipeline}} T1 {{else}} T0 {{end}}
|
||||
If the value of the pipeline is empty, dot is unaffected and T0
|
||||
is executed; otherwise, dot is set to the value of the pipeline
|
||||
and T1 is executed.
|
||||
|
||||
Arguments
|
||||
|
||||
An argument is a simple value, denoted by one of the following.
|
||||
|
||||
- A boolean, string, character, integer, floating-point, imaginary
|
||||
or complex constant in Go syntax. These behave like Go's untyped
|
||||
constants. Note that, as in Go, whether a large integer constant
|
||||
overflows when assigned or passed to a function can depend on whether
|
||||
the host machine's ints are 32 or 64 bits.
|
||||
- The keyword nil, representing an untyped Go nil.
|
||||
- The character '.' (period):
|
||||
.
|
||||
The result is the value of dot.
|
||||
- A variable name, which is a (possibly empty) alphanumeric string
|
||||
preceded by a dollar sign, such as
|
||||
$piOver2
|
||||
or
|
||||
$
|
||||
The result is the value of the variable.
|
||||
Variables are described below.
|
||||
- The name of a field of the data, which must be a struct, preceded
|
||||
by a period, such as
|
||||
.Field
|
||||
The result is the value of the field. Field invocations may be
|
||||
chained:
|
||||
.Field1.Field2
|
||||
Fields can also be evaluated on variables, including chaining:
|
||||
$x.Field1.Field2
|
||||
- The name of a key of the data, which must be a map, preceded
|
||||
by a period, such as
|
||||
.Key
|
||||
The result is the map element value indexed by the key.
|
||||
Key invocations may be chained and combined with fields to any
|
||||
depth:
|
||||
.Field1.Key1.Field2.Key2
|
||||
Although the key must be an alphanumeric identifier, unlike with
|
||||
field names they do not need to start with an upper case letter.
|
||||
Keys can also be evaluated on variables, including chaining:
|
||||
$x.key1.key2
|
||||
- The name of a niladic method of the data, preceded by a period,
|
||||
such as
|
||||
.Method
|
||||
The result is the value of invoking the method with dot as the
|
||||
receiver, dot.Method(). Such a method must have one return value (of
|
||||
any type) or two return values, the second of which is an error.
|
||||
If it has two and the returned error is non-nil, execution terminates
|
||||
and an error is returned to the caller as the value of Execute.
|
||||
Method invocations may be chained and combined with fields and keys
|
||||
to any depth:
|
||||
.Field1.Key1.Method1.Field2.Key2.Method2
|
||||
Methods can also be evaluated on variables, including chaining:
|
||||
$x.Method1.Field
|
||||
- The name of a niladic function, such as
|
||||
fun
|
||||
The result is the value of invoking the function, fun(). The return
|
||||
types and values behave as in methods. Functions and function
|
||||
names are described below.
|
||||
- A parenthesized instance of one the above, for grouping. The result
|
||||
may be accessed by a field or map key invocation.
|
||||
print (.F1 arg1) (.F2 arg2)
|
||||
(.StructValuedMethod "arg").Field
|
||||
|
||||
Arguments may evaluate to any type; if they are pointers the implementation
|
||||
automatically indirects to the base type when required.
|
||||
If an evaluation yields a function value, such as a function-valued
|
||||
field of a struct, the function is not invoked automatically, but it
|
||||
can be used as a truth value for an if action and the like. To invoke
|
||||
it, use the call function, defined below.
|
||||
|
||||
Pipelines
|
||||
|
||||
A pipeline is a possibly chained sequence of "commands". A command is a simple
|
||||
value (argument) or a function or method call, possibly with multiple arguments:
|
||||
|
||||
Argument
|
||||
The result is the value of evaluating the argument.
|
||||
.Method [Argument...]
|
||||
The method can be alone or the last element of a chain but,
|
||||
unlike methods in the middle of a chain, it can take arguments.
|
||||
The result is the value of calling the method with the
|
||||
arguments:
|
||||
dot.Method(Argument1, etc.)
|
||||
functionName [Argument...]
|
||||
The result is the value of calling the function associated
|
||||
with the name:
|
||||
function(Argument1, etc.)
|
||||
Functions and function names are described below.
|
||||
|
||||
A pipeline may be "chained" by separating a sequence of commands with pipeline
|
||||
characters '|'. In a chained pipeline, the result of each command is
|
||||
passed as the last argument of the following command. The output of the final
|
||||
command in the pipeline is the value of the pipeline.
|
||||
|
||||
The output of a command will be either one value or two values, the second of
|
||||
which has type error. If that second value is present and evaluates to
|
||||
non-nil, execution terminates and the error is returned to the caller of
|
||||
Execute.
|
||||
|
||||
Variables
|
||||
|
||||
A pipeline inside an action may initialize a variable to capture the result.
|
||||
The initialization has syntax
|
||||
|
||||
$variable := pipeline
|
||||
|
||||
where $variable is the name of the variable. An action that declares a
|
||||
variable produces no output.
|
||||
|
||||
Variables previously declared can also be assigned, using the syntax
|
||||
|
||||
$variable = pipeline
|
||||
|
||||
If a "range" action initializes a variable, the variable is set to the
|
||||
successive elements of the iteration. Also, a "range" may declare two
|
||||
variables, separated by a comma:
|
||||
|
||||
range $index, $element := pipeline
|
||||
|
||||
in which case $index and $element are set to the successive values of the
|
||||
array/slice index or map key and element, respectively. Note that if there is
|
||||
only one variable, it is assigned the element; this is opposite to the
|
||||
convention in Go range clauses.
|
||||
|
||||
A variable's scope extends to the "end" action of the control structure ("if",
|
||||
"with", or "range") in which it is declared, or to the end of the template if
|
||||
there is no such control structure. A template invocation does not inherit
|
||||
variables from the point of its invocation.
|
||||
|
||||
When execution begins, $ is set to the data argument passed to Execute, that is,
|
||||
to the starting value of dot.
|
||||
|
||||
Examples
|
||||
|
||||
Here are some example one-line templates demonstrating pipelines and variables.
|
||||
All produce the quoted word "output":
|
||||
|
||||
{{"\"output\""}}
|
||||
A string constant.
|
||||
{{`"output"`}}
|
||||
A raw string constant.
|
||||
{{printf "%q" "output"}}
|
||||
A function call.
|
||||
{{"output" | printf "%q"}}
|
||||
A function call whose final argument comes from the previous
|
||||
command.
|
||||
{{printf "%q" (print "out" "put")}}
|
||||
A parenthesized argument.
|
||||
{{"put" | printf "%s%s" "out" | printf "%q"}}
|
||||
A more elaborate call.
|
||||
{{"output" | printf "%s" | printf "%q"}}
|
||||
A longer chain.
|
||||
{{with "output"}}{{printf "%q" .}}{{end}}
|
||||
A with action using dot.
|
||||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
|
||||
A with action that creates and uses a variable.
|
||||
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
|
||||
A with action that uses the variable in another action.
|
||||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
|
||||
The same, but pipelined.
|
||||
|
||||
Functions
|
||||
|
||||
During execution functions are found in two function maps: first in the
|
||||
template, then in the global function map. By default, no functions are defined
|
||||
in the template but the Funcs method can be used to add them.
|
||||
|
||||
Predefined global functions are named as follows.
|
||||
|
||||
and
|
||||
Returns the boolean AND of its arguments by returning the
|
||||
first empty argument or the last argument, that is,
|
||||
"and x y" behaves as "if x then y else x". All the
|
||||
arguments are evaluated.
|
||||
call
|
||||
Returns the result of calling the first argument, which
|
||||
must be a function, with the remaining arguments as parameters.
|
||||
Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where
|
||||
Y is a func-valued field, map entry, or the like.
|
||||
The first argument must be the result of an evaluation
|
||||
that yields a value of function type (as distinct from
|
||||
a predefined function such as print). The function must
|
||||
return either one or two result values, the second of which
|
||||
is of type error. If the arguments don't match the function
|
||||
or the returned error value is non-nil, execution stops.
|
||||
html
|
||||
Returns the escaped HTML equivalent of the textual
|
||||
representation of its arguments. This function is unavailable
|
||||
in html/template, with a few exceptions.
|
||||
index
|
||||
Returns the result of indexing its first argument by the
|
||||
following 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.
|
||||
slice
|
||||
slice returns the result of slicing its first argument by the
|
||||
remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2],
|
||||
while "slice x" 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.
|
||||
js
|
||||
Returns the escaped JavaScript equivalent of the textual
|
||||
representation of its arguments.
|
||||
len
|
||||
Returns the integer length of its argument.
|
||||
not
|
||||
Returns the boolean negation of its single argument.
|
||||
or
|
||||
Returns the boolean OR of its arguments by returning the
|
||||
first non-empty argument or the last argument, that is,
|
||||
"or x y" behaves as "if x then x else y". All the
|
||||
arguments are evaluated.
|
||||
print
|
||||
An alias for fmt.Sprint
|
||||
printf
|
||||
An alias for fmt.Sprintf
|
||||
println
|
||||
An alias for fmt.Sprintln
|
||||
urlquery
|
||||
Returns the escaped value of the textual representation of
|
||||
its arguments in a form suitable for embedding in a URL query.
|
||||
This function is unavailable in html/template, with a few
|
||||
exceptions.
|
||||
|
||||
The boolean functions take any zero value to be false and a non-zero
|
||||
value to be true.
|
||||
|
||||
There is also a set of binary comparison operators defined as
|
||||
functions:
|
||||
|
||||
eq
|
||||
Returns the boolean truth of arg1 == arg2
|
||||
ne
|
||||
Returns the boolean truth of arg1 != arg2
|
||||
lt
|
||||
Returns the boolean truth of arg1 < arg2
|
||||
le
|
||||
Returns the boolean truth of arg1 <= arg2
|
||||
gt
|
||||
Returns the boolean truth of arg1 > arg2
|
||||
ge
|
||||
Returns the boolean truth of arg1 >= arg2
|
||||
|
||||
For simpler multi-way equality tests, eq (only) accepts two or more
|
||||
arguments and compares the second and subsequent to the first,
|
||||
returning in effect
|
||||
|
||||
arg1==arg2 || arg1==arg3 || arg1==arg4 ...
|
||||
|
||||
(Unlike with || in Go, however, eq is a function call and all the
|
||||
arguments will be evaluated.)
|
||||
|
||||
The comparison functions work on basic types only (or named basic
|
||||
types, such as "type Celsius float32"). They implement the Go rules
|
||||
for comparison of values, except that size and exact type are
|
||||
ignored, so any integer value, signed or unsigned, may be compared
|
||||
with any other integer value. (The arithmetic value is compared,
|
||||
not the bit pattern, so all negative integers are less than all
|
||||
unsigned integers.) However, as usual, one may not compare an int
|
||||
with a float32 and so on.
|
||||
|
||||
Associated templates
|
||||
|
||||
Each template is named by a string specified when it is created. Also, each
|
||||
template is associated with zero or more other templates that it may invoke by
|
||||
name; such associations are transitive and form a name space of templates.
|
||||
|
||||
A template may use a template invocation to instantiate another associated
|
||||
template; see the explanation of the "template" action above. The name must be
|
||||
that of a template associated with the template that contains the invocation.
|
||||
|
||||
Nested template definitions
|
||||
|
||||
When parsing a template, another template may be defined and associated with the
|
||||
template being parsed. Template definitions must appear at the top level of the
|
||||
template, much like global variables in a Go program.
|
||||
|
||||
The syntax of such definitions is to surround each template declaration with a
|
||||
"define" and "end" action.
|
||||
|
||||
The define action names the template being created by providing a string
|
||||
constant. Here is a simple example:
|
||||
|
||||
`{{define "T1"}}ONE{{end}}
|
||||
{{define "T2"}}TWO{{end}}
|
||||
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
|
||||
{{template "T3"}}`
|
||||
|
||||
This defines two templates, T1 and T2, and a third T3 that invokes the other two
|
||||
when it is executed. Finally it invokes T3. If executed this template will
|
||||
produce the text
|
||||
|
||||
ONE TWO
|
||||
|
||||
By construction, a template may reside in only one association. If it's
|
||||
necessary to have a template addressable from multiple associations, the
|
||||
template definition must be parsed multiple times to create distinct *Template
|
||||
values, or must be copied with the Clone or AddParseTree method.
|
||||
|
||||
Parse may be called multiple times to assemble the various associated templates;
|
||||
see the ParseFiles and ParseGlob functions and methods for simple ways to parse
|
||||
related templates stored in files.
|
||||
|
||||
A template may be executed directly or through ExecuteTemplate, which executes
|
||||
an associated template identified by name. To invoke our example above, we
|
||||
might write,
|
||||
|
||||
err := tmpl.Execute(os.Stdout, "no data needed")
|
||||
if err != nil {
|
||||
log.Fatalf("execution failed: %s", err)
|
||||
}
|
||||
|
||||
or to invoke a particular template explicitly by name,
|
||||
|
||||
err := tmpl.ExecuteTemplate(os.Stdout, "T2", "no data needed")
|
||||
if err != nil {
|
||||
log.Fatalf("execution failed: %s", err)
|
||||
}
|
||||
|
||||
*/
|
||||
package template
|
112
tpl/internal/go_templates/texttemplate/example_test.go
Normal file
112
tpl/internal/go_templates/texttemplate/example_test.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2011 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 go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func ExampleTemplate() {
|
||||
// Define a template.
|
||||
const letter = `
|
||||
Dear {{.Name}},
|
||||
{{if .Attended}}
|
||||
It was a pleasure to see you at the wedding.
|
||||
{{- else}}
|
||||
It is a shame you couldn't make it to the wedding.
|
||||
{{- end}}
|
||||
{{with .Gift -}}
|
||||
Thank you for the lovely {{.}}.
|
||||
{{end}}
|
||||
Best wishes,
|
||||
Josie
|
||||
`
|
||||
|
||||
// Prepare some data to insert into the template.
|
||||
type Recipient struct {
|
||||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
var recipients = []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
}
|
||||
|
||||
// Create a new template and parse the letter into it.
|
||||
t := template.Must(template.New("letter").Parse(letter))
|
||||
|
||||
// Execute the template for each recipient.
|
||||
for _, r := range recipients {
|
||||
err := t.Execute(os.Stdout, r)
|
||||
if err != nil {
|
||||
log.Println("executing template:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Dear Aunt Mildred,
|
||||
//
|
||||
// It was a pleasure to see you at the wedding.
|
||||
// Thank you for the lovely bone china tea set.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Uncle John,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
// Thank you for the lovely moleskin pants.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Cousin Rodney,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
}
|
||||
|
||||
// The following example is duplicated in html/template; keep them in sync.
|
||||
|
||||
func ExampleTemplate_block() {
|
||||
const (
|
||||
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
|
||||
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
|
||||
)
|
||||
var (
|
||||
funcs = template.FuncMap{"join": strings.Join}
|
||||
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
|
||||
)
|
||||
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// Names:
|
||||
// - Gamora
|
||||
// - Groot
|
||||
// - Nebula
|
||||
// - Rocket
|
||||
// - Star-Lord
|
||||
// Names: Gamora, Groot, Nebula, Rocket, Star-Lord
|
||||
}
|
184
tpl/internal/go_templates/texttemplate/examplefiles_test.go
Normal file
184
tpl/internal/go_templates/texttemplate/examplefiles_test.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
// Copyright 2012 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 go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// templateFile defines the contents of a template to be stored in a file, for testing.
|
||||
type templateFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func createTestDir(files []templateFile) string {
|
||||
dir, err := ioutil.TempDir("", "template")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
f, err := os.Create(filepath.Join(dir, file.name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.WriteString(f, file.contents)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// Here we demonstrate loading a set of templates from a directory.
|
||||
func ExampleTemplate_glob() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// T0.tmpl is the first name matched, so it becomes the starting template,
|
||||
// the value returned by ParseGlob.
|
||||
tmpl := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
err := tmpl.Execute(os.Stdout, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("template execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// T0 invokes T1: (T1 invokes T2: (This is T2))
|
||||
}
|
||||
|
||||
// This example demonstrates one way to share some templates
|
||||
// and use them in different contexts. In this variant we add multiple driver
|
||||
// templates by hand to an existing bundle of templates.
|
||||
func ExampleTemplate_helpers() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the helpers.
|
||||
templates := template.Must(template.ParseGlob(pattern))
|
||||
// Add one driver template to the bunch; we do this with an explicit template definition.
|
||||
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver1: ", err)
|
||||
}
|
||||
// Add another driver template.
|
||||
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver2: ", err)
|
||||
}
|
||||
// We load all the templates before execution. This package does not require
|
||||
// that behavior but html/template's escaping does, so it's a good habit.
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver1 execution: %s", err)
|
||||
}
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver2 execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// Driver 1 calls T1: (T1 invokes T2: (This is T2))
|
||||
// Driver 2 calls T2: (This is T2)
|
||||
}
|
||||
|
||||
// This example demonstrates how to use one group of driver
|
||||
// templates with distinct sets of helper templates.
|
||||
func ExampleTemplate_share() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
|
||||
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the drivers.
|
||||
drivers := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
// We must define an implementation of the T2 template. First we clone
|
||||
// the drivers, then add a definition of T2 to the template name space.
|
||||
|
||||
// 1. Clone the helper set to create a new name space from which to run them.
|
||||
first, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning helpers: ", err)
|
||||
}
|
||||
// 2. Define T2, version A, and parse it.
|
||||
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Now repeat the whole thing, using a different version of T2.
|
||||
// 1. Clone the drivers.
|
||||
second, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning drivers: ", err)
|
||||
}
|
||||
// 2. Define T2, version B, and parse it.
|
||||
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Execute the templates in the reverse order to verify the
|
||||
// first is unaffected by the second.
|
||||
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
|
||||
if err != nil {
|
||||
log.Fatalf("second execution: %s", err)
|
||||
}
|
||||
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
|
||||
if err != nil {
|
||||
log.Fatalf("first: execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
|
||||
// T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
|
||||
}
|
56
tpl/internal/go_templates/texttemplate/examplefunc_test.go
Normal file
56
tpl/internal/go_templates/texttemplate/examplefunc_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2012 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 go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// This example demonstrates a custom function to process template text.
|
||||
// It installs the strings.Title function and uses it to
|
||||
// Make Title Text Look Good In Our Template's Output.
|
||||
func ExampleTemplate_func() {
|
||||
// First we create a FuncMap with which to register the function.
|
||||
funcMap := template.FuncMap{
|
||||
// The name "title" is what the function will be called in the template text.
|
||||
"title": strings.Title,
|
||||
}
|
||||
|
||||
// A simple template definition to test our function.
|
||||
// We print the input text several ways:
|
||||
// - the original
|
||||
// - title-cased
|
||||
// - title-cased and then printed with %q
|
||||
// - printed with %q and then title-cased.
|
||||
const templateText = `
|
||||
Input: {{printf "%q" .}}
|
||||
Output 0: {{title .}}
|
||||
Output 1: {{title . | printf "%q"}}
|
||||
Output 2: {{printf "%q" . | title}}
|
||||
`
|
||||
|
||||
// Create a template, add the function map, and parse the text.
|
||||
tmpl, err := template.New("titleTest").Funcs(funcMap).Parse(templateText)
|
||||
if err != nil {
|
||||
log.Fatalf("parsing: %s", err)
|
||||
}
|
||||
|
||||
// Run the template to verify the output.
|
||||
err = tmpl.Execute(os.Stdout, "the go programming language")
|
||||
if err != nil {
|
||||
log.Fatalf("execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Input: "the go programming language"
|
||||
// Output 0: The Go Programming Language
|
||||
// Output 1: "The Go Programming Language"
|
||||
// Output 2: "The Go Programming Language"
|
||||
}
|
980
tpl/internal/go_templates/texttemplate/exec.go
Normal file
980
tpl/internal/go_templates/texttemplate/exec.go
Normal file
|
@ -0,0 +1,980 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// maxExecDepth specifies the maximum stack depth of templates within
|
||||
// templates. This limit is only practically reached by accidentally
|
||||
// recursive template invocations. This limit allows us to return
|
||||
// an error instead of triggering a stack overflow.
|
||||
var maxExecDepth = initMaxExecDepth()
|
||||
|
||||
func initMaxExecDepth() int {
|
||||
if runtime.GOARCH == "wasm" {
|
||||
return 1000
|
||||
}
|
||||
return 100000
|
||||
}
|
||||
|
||||
// state represents the state of an execution. It's not part of the
|
||||
// template so that multiple executions of the same template
|
||||
// can execute in parallel.
|
||||
type stateOld struct {
|
||||
tmpl *Template
|
||||
wr io.Writer
|
||||
node parse.Node // current node, for errors
|
||||
vars []variable // push-down stack of variable values.
|
||||
depth int // the height of the stack of executing templates.
|
||||
}
|
||||
|
||||
// variable holds the dynamic value of a variable such as $, $x etc.
|
||||
type variable struct {
|
||||
name string
|
||||
value reflect.Value
|
||||
}
|
||||
|
||||
// push pushes a new variable on the stack.
|
||||
func (s *state) push(name string, value reflect.Value) {
|
||||
s.vars = append(s.vars, variable{name, value})
|
||||
}
|
||||
|
||||
// mark returns the length of the variable stack.
|
||||
func (s *state) mark() int {
|
||||
return len(s.vars)
|
||||
}
|
||||
|
||||
// pop pops the variable stack up to the mark.
|
||||
func (s *state) pop(mark int) {
|
||||
s.vars = s.vars[0:mark]
|
||||
}
|
||||
|
||||
// setVar overwrites the last declared variable with the given name.
|
||||
// Used by variable assignments.
|
||||
func (s *state) setVar(name string, value reflect.Value) {
|
||||
for i := s.mark() - 1; i >= 0; i-- {
|
||||
if s.vars[i].name == name {
|
||||
s.vars[i].value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
s.errorf("undefined variable: %s", name)
|
||||
}
|
||||
|
||||
// setTopVar overwrites the top-nth variable on the stack. Used by range iterations.
|
||||
func (s *state) setTopVar(n int, value reflect.Value) {
|
||||
s.vars[len(s.vars)-n].value = value
|
||||
}
|
||||
|
||||
// varValue returns the value of the named variable.
|
||||
func (s *state) varValue(name string) reflect.Value {
|
||||
for i := s.mark() - 1; i >= 0; i-- {
|
||||
if s.vars[i].name == name {
|
||||
return s.vars[i].value
|
||||
}
|
||||
}
|
||||
s.errorf("undefined variable: %s", name)
|
||||
return zero
|
||||
}
|
||||
|
||||
var zero reflect.Value
|
||||
|
||||
type missingValType struct{}
|
||||
|
||||
var missingVal = reflect.ValueOf(missingValType{})
|
||||
|
||||
// at marks the state to be on node n, for error reporting.
|
||||
func (s *state) at(node parse.Node) {
|
||||
s.node = node
|
||||
}
|
||||
|
||||
// doublePercent returns the string with %'s replaced by %%, if necessary,
|
||||
// so it can be used safely inside a Printf format string.
|
||||
func doublePercent(str string) string {
|
||||
return strings.ReplaceAll(str, "%", "%%")
|
||||
}
|
||||
|
||||
// TODO: It would be nice if ExecError was more broken down, but
|
||||
// the way ErrorContext embeds the template name makes the
|
||||
// processing too clumsy.
|
||||
|
||||
// ExecError is the custom error type returned when Execute has an
|
||||
// error evaluating its template. (If a write error occurs, the actual
|
||||
// error is returned; it will not be of type ExecError.)
|
||||
type ExecError struct {
|
||||
Name string // Name of template.
|
||||
Err error // Pre-formatted error.
|
||||
}
|
||||
|
||||
func (e ExecError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e ExecError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// errorf records an ExecError and terminates processing.
|
||||
func (s *state) errorf(format string, args ...interface{}) {
|
||||
name := doublePercent(s.tmpl.Name())
|
||||
if s.node == nil {
|
||||
format = fmt.Sprintf("template: %s: %s", name, format)
|
||||
} else {
|
||||
location, context := s.tmpl.ErrorContext(s.node)
|
||||
format = fmt.Sprintf("template: %s: executing %q at <%s>: %s", location, name, doublePercent(context), format)
|
||||
}
|
||||
panic(ExecError{
|
||||
Name: s.tmpl.Name(),
|
||||
Err: fmt.Errorf(format, args...),
|
||||
})
|
||||
}
|
||||
|
||||
// writeError is the wrapper type used internally when Execute has an
|
||||
// error writing to its output. We strip the wrapper in errRecover.
|
||||
// Note that this is not an implementation of error, so it cannot escape
|
||||
// from the package as an error value.
|
||||
type writeError struct {
|
||||
Err error // Original error.
|
||||
}
|
||||
|
||||
func (s *state) writeError(err error) {
|
||||
panic(writeError{
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
|
||||
// errRecover is the handler that turns panics into returns from the top
|
||||
// level of Parse.
|
||||
func errRecover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
switch err := e.(type) {
|
||||
case runtime.Error:
|
||||
panic(e)
|
||||
case writeError:
|
||||
*errp = err.Err // Strip the wrapper.
|
||||
case ExecError:
|
||||
*errp = err // Keep the wrapper.
|
||||
default:
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteTemplate applies the template associated with t that has the given name
|
||||
// to the specified data object and writes the output to wr.
|
||||
// If an error occurs executing the template or writing its output,
|
||||
// execution stops, but partial results may already have been written to
|
||||
// the output writer.
|
||||
// A template may be executed safely in parallel, although if parallel
|
||||
// executions share a Writer the output may be interleaved.
|
||||
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
|
||||
var tmpl *Template
|
||||
if t.common != nil {
|
||||
tmpl = t.tmpl[name]
|
||||
}
|
||||
if tmpl == nil {
|
||||
return fmt.Errorf("template: no template %q associated with template %q", name, t.name)
|
||||
}
|
||||
return tmpl.Execute(wr, data)
|
||||
}
|
||||
|
||||
// Execute applies a parsed template to the specified data object,
|
||||
// and writes the output to wr.
|
||||
// If an error occurs executing the template or writing its output,
|
||||
// execution stops, but partial results may already have been written to
|
||||
// the output writer.
|
||||
// A template may be executed safely in parallel, although if parallel
|
||||
// executions share a Writer the output may be interleaved.
|
||||
//
|
||||
// If data is a reflect.Value, the template applies to the concrete
|
||||
// value that the reflect.Value holds, as in fmt.Print.
|
||||
func (t *Template) Execute(wr io.Writer, data interface{}) error {
|
||||
return t.execute(wr, data)
|
||||
}
|
||||
|
||||
func (t *Template) execute(wr io.Writer, data interface{}) (err error) {
|
||||
defer errRecover(&err)
|
||||
value, ok := data.(reflect.Value)
|
||||
if !ok {
|
||||
value = reflect.ValueOf(data)
|
||||
}
|
||||
state := &state{
|
||||
tmpl: t,
|
||||
wr: wr,
|
||||
vars: []variable{{"$", value}},
|
||||
}
|
||||
if t.Tree == nil || t.Root == nil {
|
||||
state.errorf("%q is an incomplete or empty template", t.Name())
|
||||
}
|
||||
state.walk(value, t.Root)
|
||||
return
|
||||
}
|
||||
|
||||
// DefinedTemplates returns a string listing the defined templates,
|
||||
// prefixed by the string "; defined templates are: ". If there are none,
|
||||
// it returns the empty string. For generating an error message here
|
||||
// and in html/template.
|
||||
func (t *Template) DefinedTemplates() string {
|
||||
if t.common == nil {
|
||||
return ""
|
||||
}
|
||||
var b bytes.Buffer
|
||||
for name, tmpl := range t.tmpl {
|
||||
if tmpl.Tree == nil || tmpl.Root == nil {
|
||||
continue
|
||||
}
|
||||
if b.Len() > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&b, "%q", name)
|
||||
}
|
||||
var s string
|
||||
if b.Len() > 0 {
|
||||
s = "; defined templates are: " + b.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Walk functions step through the major pieces of the template structure,
|
||||
// generating output as they go.
|
||||
func (s *state) walk(dot reflect.Value, node parse.Node) {
|
||||
s.at(node)
|
||||
switch node := node.(type) {
|
||||
case *parse.ActionNode:
|
||||
// Do not pop variables so they persist until next end.
|
||||
// Also, if the action declares variables, don't print the result.
|
||||
val := s.evalPipeline(dot, node.Pipe)
|
||||
if len(node.Pipe.Decl) == 0 {
|
||||
s.printValue(node, val)
|
||||
}
|
||||
case *parse.IfNode:
|
||||
s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
|
||||
case *parse.ListNode:
|
||||
for _, node := range node.Nodes {
|
||||
s.walk(dot, node)
|
||||
}
|
||||
case *parse.RangeNode:
|
||||
s.walkRange(dot, node)
|
||||
case *parse.TemplateNode:
|
||||
s.walkTemplate(dot, node)
|
||||
case *parse.TextNode:
|
||||
if _, err := s.wr.Write(node.Text); err != nil {
|
||||
s.writeError(err)
|
||||
}
|
||||
case *parse.WithNode:
|
||||
s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
|
||||
default:
|
||||
s.errorf("unknown node: %s", node)
|
||||
}
|
||||
}
|
||||
|
||||
// walkIfOrWith walks an 'if' or 'with' node. The two control structures
|
||||
// are identical in behavior except that 'with' sets dot.
|
||||
func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) {
|
||||
defer s.pop(s.mark())
|
||||
val := s.evalPipeline(dot, pipe)
|
||||
truth, ok := isTrue(indirectInterface(val))
|
||||
if !ok {
|
||||
s.errorf("if/with can't use %v", val)
|
||||
}
|
||||
if truth {
|
||||
if typ == parse.NodeWith {
|
||||
s.walk(val, list)
|
||||
} else {
|
||||
s.walk(dot, list)
|
||||
}
|
||||
} else if elseList != nil {
|
||||
s.walk(dot, elseList)
|
||||
}
|
||||
}
|
||||
|
||||
// IsTrue reports whether the value is 'true', in the sense of not the zero of its type,
|
||||
// and whether the value has a meaningful truth value. This is the definition of
|
||||
// truth used by if and other such actions.
|
||||
func IsTrue(val interface{}) (truth, ok bool) {
|
||||
return isTrue(reflect.ValueOf(val))
|
||||
}
|
||||
|
||||
func isTrue(val reflect.Value) (truth, ok bool) {
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return false, true
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
truth = val.Len() > 0
|
||||
case reflect.Bool:
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
truth = val.Float() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
truth = val.Uint() != 0
|
||||
case reflect.Struct:
|
||||
truth = true // Struct values are always true.
|
||||
default:
|
||||
return
|
||||
}
|
||||
return truth, true
|
||||
}
|
||||
|
||||
func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||
s.at(r)
|
||||
defer s.pop(s.mark())
|
||||
val, _ := indirect(s.evalPipeline(dot, r.Pipe))
|
||||
// mark top of stack before any variables in the body are pushed.
|
||||
mark := s.mark()
|
||||
oneIteration := func(index, elem reflect.Value) {
|
||||
// Set top var (lexically the second if there are two) to the element.
|
||||
if len(r.Pipe.Decl) > 0 {
|
||||
s.setTopVar(1, elem)
|
||||
}
|
||||
// Set next var (lexically the first if there are two) to the index.
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
s.setTopVar(2, index)
|
||||
}
|
||||
s.walk(elem, r.List)
|
||||
s.pop(mark)
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if val.Len() == 0 {
|
||||
break
|
||||
}
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
oneIteration(reflect.ValueOf(i), val.Index(i))
|
||||
}
|
||||
return
|
||||
case reflect.Map:
|
||||
if val.Len() == 0 {
|
||||
break
|
||||
}
|
||||
om := fmtsort.Sort(val)
|
||||
for i, key := range om.Key {
|
||||
oneIteration(key, om.Value[i])
|
||||
}
|
||||
return
|
||||
case reflect.Chan:
|
||||
if val.IsNil() {
|
||||
break
|
||||
}
|
||||
i := 0
|
||||
for ; ; i++ {
|
||||
elem, ok := val.Recv()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
oneIteration(reflect.ValueOf(i), elem)
|
||||
}
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
return
|
||||
case reflect.Invalid:
|
||||
break // An invalid value is likely a nil map, etc. and acts like an empty map.
|
||||
default:
|
||||
s.errorf("range can't iterate over %v", val)
|
||||
}
|
||||
if r.ElseList != nil {
|
||||
s.walk(dot, r.ElseList)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
|
||||
s.at(t)
|
||||
tmpl := s.tmpl.tmpl[t.Name]
|
||||
if tmpl == nil {
|
||||
s.errorf("template %q not defined", t.Name)
|
||||
}
|
||||
if s.depth == maxExecDepth {
|
||||
s.errorf("exceeded maximum template depth (%v)", maxExecDepth)
|
||||
}
|
||||
// Variables declared by the pipeline persist.
|
||||
dot = s.evalPipeline(dot, t.Pipe)
|
||||
newState := *s
|
||||
newState.depth++
|
||||
newState.tmpl = tmpl
|
||||
// No dynamic scoping: template invocations inherit no variables.
|
||||
newState.vars = []variable{{"$", dot}}
|
||||
newState.walk(dot, tmpl.Root)
|
||||
}
|
||||
|
||||
// Eval functions evaluate pipelines, commands, and their elements and extract
|
||||
// values from the data structure by examining fields, calling methods, and so on.
|
||||
// The printing of those values happens only through walk functions.
|
||||
|
||||
// evalPipeline returns the value acquired by evaluating a pipeline. If the
|
||||
// pipeline has a variable declaration, the variable will be pushed on the
|
||||
// stack. Callers should therefore pop the stack after they are finished
|
||||
// executing commands depending on the pipeline value.
|
||||
func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value reflect.Value) {
|
||||
if pipe == nil {
|
||||
return
|
||||
}
|
||||
s.at(pipe)
|
||||
value = missingVal
|
||||
for _, cmd := range pipe.Cmds {
|
||||
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
|
||||
// If the object has type interface{}, dig down one level to the thing inside.
|
||||
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
||||
value = reflect.ValueOf(value.Interface()) // lovely!
|
||||
}
|
||||
}
|
||||
for _, variable := range pipe.Decl {
|
||||
if pipe.IsAssign {
|
||||
s.setVar(variable.Ident[0], value)
|
||||
} else {
|
||||
s.push(variable.Ident[0], value)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *state) notAFunction(args []parse.Node, final reflect.Value) {
|
||||
if len(args) > 1 || final != missingVal {
|
||||
s.errorf("can't give argument to non-function %s", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final reflect.Value) reflect.Value {
|
||||
firstWord := cmd.Args[0]
|
||||
switch n := firstWord.(type) {
|
||||
case *parse.FieldNode:
|
||||
return s.evalFieldNode(dot, n, cmd.Args, final)
|
||||
case *parse.ChainNode:
|
||||
return s.evalChainNode(dot, n, cmd.Args, final)
|
||||
case *parse.IdentifierNode:
|
||||
// Must be a function.
|
||||
return s.evalFunction(dot, n, cmd, cmd.Args, final)
|
||||
case *parse.PipeNode:
|
||||
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
|
||||
return s.evalPipeline(dot, n)
|
||||
case *parse.VariableNode:
|
||||
return s.evalVariableNode(dot, n, cmd.Args, final)
|
||||
}
|
||||
s.at(firstWord)
|
||||
s.notAFunction(cmd.Args, final)
|
||||
switch word := firstWord.(type) {
|
||||
case *parse.BoolNode:
|
||||
return reflect.ValueOf(word.True)
|
||||
case *parse.DotNode:
|
||||
return dot
|
||||
case *parse.NilNode:
|
||||
s.errorf("nil is not a command")
|
||||
case *parse.NumberNode:
|
||||
return s.idealConstant(word)
|
||||
case *parse.StringNode:
|
||||
return reflect.ValueOf(word.Text)
|
||||
}
|
||||
s.errorf("can't evaluate command %q", firstWord)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// idealConstant is called to return the value of a number in a context where
|
||||
// we don't know the type. In that case, the syntax of the number tells us
|
||||
// its type, and we use Go rules to resolve. Note there is no such thing as
|
||||
// a uint ideal constant in this situation - the value must be of int type.
|
||||
func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
|
||||
// These are ideal constants but we don't know the type
|
||||
// and we have no context. (If it was a method argument,
|
||||
// we'd know what we need.) The syntax guides us to some extent.
|
||||
s.at(constant)
|
||||
switch {
|
||||
case constant.IsComplex:
|
||||
return reflect.ValueOf(constant.Complex128) // incontrovertible.
|
||||
case constant.IsFloat && !isHexInt(constant.Text) && strings.ContainsAny(constant.Text, ".eEpP"):
|
||||
return reflect.ValueOf(constant.Float64)
|
||||
case constant.IsInt:
|
||||
n := int(constant.Int64)
|
||||
if int64(n) != constant.Int64 {
|
||||
s.errorf("%s overflows int", constant.Text)
|
||||
}
|
||||
return reflect.ValueOf(n)
|
||||
case constant.IsUint:
|
||||
s.errorf("%s overflows int", constant.Text)
|
||||
}
|
||||
return zero
|
||||
}
|
||||
|
||||
func isHexInt(s string) bool {
|
||||
return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP")
|
||||
}
|
||||
|
||||
func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(field)
|
||||
return s.evalFieldChain(dot, dot, field, field.Ident, args, final)
|
||||
}
|
||||
|
||||
func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(chain)
|
||||
if len(chain.Field) == 0 {
|
||||
s.errorf("internal error: no fields in evalChainNode")
|
||||
}
|
||||
if chain.Node.Type() == parse.NodeNil {
|
||||
s.errorf("indirection through explicit nil in %s", chain)
|
||||
}
|
||||
// (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields.
|
||||
pipe := s.evalArg(dot, nil, chain.Node)
|
||||
return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final)
|
||||
}
|
||||
|
||||
func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
|
||||
s.at(variable)
|
||||
value := s.varValue(variable.Ident[0])
|
||||
if len(variable.Ident) == 1 {
|
||||
s.notAFunction(args, final)
|
||||
return value
|
||||
}
|
||||
return s.evalFieldChain(dot, value, variable, variable.Ident[1:], args, final)
|
||||
}
|
||||
|
||||
// evalFieldChain evaluates .X.Y.Z possibly followed by arguments.
|
||||
// dot is the environment in which to evaluate arguments, while
|
||||
// receiver is the value being walked along the chain.
|
||||
func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ident []string, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
n := len(ident)
|
||||
for i := 0; i < n-1; i++ {
|
||||
receiver = s.evalField(dot, ident[i], node, nil, missingVal, receiver)
|
||||
}
|
||||
// Now if it's a method, it gets the arguments.
|
||||
return s.evalField(dot, ident[n-1], node, args, final, receiver)
|
||||
}
|
||||
|
||||
func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(node)
|
||||
name := node.Ident
|
||||
function, ok := findFunction(name, s.tmpl)
|
||||
if !ok {
|
||||
s.errorf("%q is not a defined function", name)
|
||||
}
|
||||
return s.evalCall(dot, function, cmd, name, args, final)
|
||||
}
|
||||
|
||||
// evalField evaluates an expression like (.Field) or (.Field arg1 arg2).
|
||||
// The 'final' argument represents the return value from the preceding
|
||||
// value of the pipeline, if any.
|
||||
func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value {
|
||||
if !receiver.IsValid() {
|
||||
if s.tmpl.option.missingKey == mapError { // Treat invalid value as missing map key.
|
||||
s.errorf("nil data; no entry for key %q", fieldName)
|
||||
}
|
||||
return zero
|
||||
}
|
||||
typ := receiver.Type()
|
||||
receiver, isNil := indirect(receiver)
|
||||
if receiver.Kind() == reflect.Interface && isNil {
|
||||
// Calling a method on a nil interface can't work. The
|
||||
// MethodByName method call below would panic.
|
||||
s.errorf("nil pointer evaluating %s.%s", typ, fieldName)
|
||||
return zero
|
||||
}
|
||||
|
||||
// Unless it's an interface, need to get to a value of type *T to guarantee
|
||||
// we see all methods of T and *T.
|
||||
ptr := receiver
|
||||
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
|
||||
ptr = ptr.Addr()
|
||||
}
|
||||
if method := ptr.MethodByName(fieldName); method.IsValid() {
|
||||
return s.evalCall(dot, method, node, fieldName, args, final)
|
||||
}
|
||||
hasArgs := len(args) > 1 || final != missingVal
|
||||
// It's not a method; must be a field of a struct or an element of a map.
|
||||
switch receiver.Kind() {
|
||||
case reflect.Struct:
|
||||
tField, ok := receiver.Type().FieldByName(fieldName)
|
||||
if ok {
|
||||
field := receiver.FieldByIndex(tField.Index)
|
||||
if tField.PkgPath != "" { // field is unexported
|
||||
s.errorf("%s is an unexported field of struct type %s", fieldName, typ)
|
||||
}
|
||||
// If it's a function, we must call it.
|
||||
if hasArgs {
|
||||
s.errorf("%s has arguments but cannot be invoked as function", fieldName)
|
||||
}
|
||||
return field
|
||||
}
|
||||
case reflect.Map:
|
||||
// If it's a map, attempt to use the field name as a key.
|
||||
nameVal := reflect.ValueOf(fieldName)
|
||||
if nameVal.Type().AssignableTo(receiver.Type().Key()) {
|
||||
if hasArgs {
|
||||
s.errorf("%s is not a method but has arguments", fieldName)
|
||||
}
|
||||
result := receiver.MapIndex(nameVal)
|
||||
if !result.IsValid() {
|
||||
switch s.tmpl.option.missingKey {
|
||||
case mapInvalid:
|
||||
// Just use the invalid value.
|
||||
case mapZeroValue:
|
||||
result = reflect.Zero(receiver.Type().Elem())
|
||||
case mapError:
|
||||
s.errorf("map has no entry for key %q", fieldName)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
case reflect.Ptr:
|
||||
etyp := receiver.Type().Elem()
|
||||
if etyp.Kind() == reflect.Struct {
|
||||
if _, ok := etyp.FieldByName(fieldName); !ok {
|
||||
// If there's no such field, say "can't evaluate"
|
||||
// instead of "nil pointer evaluating".
|
||||
break
|
||||
}
|
||||
}
|
||||
if isNil {
|
||||
s.errorf("nil pointer evaluating %s.%s", typ, fieldName)
|
||||
}
|
||||
}
|
||||
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
|
||||
)
|
||||
|
||||
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
||||
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
|
||||
// as the function itself.
|
||||
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
if args != nil {
|
||||
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
||||
}
|
||||
typ := fun.Type()
|
||||
numIn := len(args)
|
||||
if final != missingVal {
|
||||
numIn++
|
||||
}
|
||||
numFixed := len(args)
|
||||
if typ.IsVariadic() {
|
||||
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
|
||||
if numIn < numFixed {
|
||||
s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args))
|
||||
}
|
||||
} else if numIn != typ.NumIn() {
|
||||
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
||||
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
||||
}
|
||||
// Build the arg list.
|
||||
argv := make([]reflect.Value, numIn)
|
||||
// Args must be evaluated. Fixed args first.
|
||||
i := 0
|
||||
for ; i < numFixed && i < len(args); i++ {
|
||||
argv[i] = s.evalArg(dot, typ.In(i), args[i])
|
||||
}
|
||||
// Now the ... args.
|
||||
if typ.IsVariadic() {
|
||||
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
|
||||
for ; i < len(args); i++ {
|
||||
argv[i] = s.evalArg(dot, argType, args[i])
|
||||
}
|
||||
}
|
||||
// Add final value if necessary.
|
||||
if final != missingVal {
|
||||
t := typ.In(typ.NumIn() - 1)
|
||||
if typ.IsVariadic() {
|
||||
if numIn-1 < numFixed {
|
||||
// The added final argument corresponds to a fixed parameter of the function.
|
||||
// Validate against the type of the actual parameter.
|
||||
t = typ.In(numIn - 1)
|
||||
} else {
|
||||
// The added final argument corresponds to the variadic part.
|
||||
// Validate against the type of the elements of the variadic slice.
|
||||
t = t.Elem()
|
||||
}
|
||||
}
|
||||
argv[i] = s.validateType(final, t)
|
||||
}
|
||||
v, err := safeCall(fun, argv)
|
||||
// If we have an error that is not nil, stop execution and return that
|
||||
// error to the caller.
|
||||
if err != nil {
|
||||
s.at(node)
|
||||
s.errorf("error calling %s: %v", name, err)
|
||||
}
|
||||
if v.Type() == reflectValueType {
|
||||
v = v.Interface().(reflect.Value)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
|
||||
func canBeNil(typ reflect.Type) bool {
|
||||
switch typ.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return true
|
||||
case reflect.Struct:
|
||||
return typ == reflectValueType
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateType guarantees that the value is valid and assignable to the type.
|
||||
func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value {
|
||||
if !value.IsValid() {
|
||||
if typ == nil {
|
||||
// An untyped nil interface{}. Accept as a proper nil value.
|
||||
return reflect.ValueOf(nil)
|
||||
}
|
||||
if canBeNil(typ) {
|
||||
// Like above, but use the zero value of the non-nil type.
|
||||
return reflect.Zero(typ)
|
||||
}
|
||||
s.errorf("invalid value; expected %s", typ)
|
||||
}
|
||||
if typ == reflectValueType && value.Type() != typ {
|
||||
return reflect.ValueOf(value)
|
||||
}
|
||||
if typ != nil && !value.Type().AssignableTo(typ) {
|
||||
if value.Kind() == reflect.Interface && !value.IsNil() {
|
||||
value = value.Elem()
|
||||
if value.Type().AssignableTo(typ) {
|
||||
return value
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
// Does one dereference or indirection work? We could do more, as we
|
||||
// do with method receivers, but that gets messy and method receivers
|
||||
// are much more constrained, so it makes more sense there than here.
|
||||
// Besides, one is almost always all you need.
|
||||
switch {
|
||||
case value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ):
|
||||
value = value.Elem()
|
||||
if !value.IsValid() {
|
||||
s.errorf("dereference of nil pointer of type %s", typ)
|
||||
}
|
||||
case reflect.PtrTo(value.Type()).AssignableTo(typ) && value.CanAddr():
|
||||
value = value.Addr()
|
||||
default:
|
||||
s.errorf("wrong type for value; expected %s; got %s", typ, value.Type())
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
switch arg := n.(type) {
|
||||
case *parse.DotNode:
|
||||
return s.validateType(dot, typ)
|
||||
case *parse.NilNode:
|
||||
if canBeNil(typ) {
|
||||
return reflect.Zero(typ)
|
||||
}
|
||||
s.errorf("cannot assign nil to %s", typ)
|
||||
case *parse.FieldNode:
|
||||
return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, missingVal), typ)
|
||||
case *parse.VariableNode:
|
||||
return s.validateType(s.evalVariableNode(dot, arg, nil, missingVal), typ)
|
||||
case *parse.PipeNode:
|
||||
return s.validateType(s.evalPipeline(dot, arg), typ)
|
||||
case *parse.IdentifierNode:
|
||||
return s.validateType(s.evalFunction(dot, arg, arg, nil, missingVal), typ)
|
||||
case *parse.ChainNode:
|
||||
return s.validateType(s.evalChainNode(dot, arg, nil, missingVal), typ)
|
||||
}
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool:
|
||||
return s.evalBool(typ, n)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return s.evalComplex(typ, n)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return s.evalFloat(typ, n)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return s.evalInteger(typ, n)
|
||||
case reflect.Interface:
|
||||
if typ.NumMethod() == 0 {
|
||||
return s.evalEmptyInterface(dot, n)
|
||||
}
|
||||
case reflect.Struct:
|
||||
if typ == reflectValueType {
|
||||
return reflect.ValueOf(s.evalEmptyInterface(dot, n))
|
||||
}
|
||||
case reflect.String:
|
||||
return s.evalString(typ, n)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return s.evalUnsignedInteger(typ, n)
|
||||
}
|
||||
s.errorf("can't handle %s for arg of type %s", n, typ)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.BoolNode); ok {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetBool(n.True)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected bool; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.StringNode); ok {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetString(n.Text)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected string; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsInt {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetInt(n.Int64)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected integer; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsUint {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetUint(n.Uint64)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected unsigned integer; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalFloat(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsFloat {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetFloat(n.Float64)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected float; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalComplex(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsComplex {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetComplex(n.Complex128)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected complex; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
switch n := n.(type) {
|
||||
case *parse.BoolNode:
|
||||
return reflect.ValueOf(n.True)
|
||||
case *parse.DotNode:
|
||||
return dot
|
||||
case *parse.FieldNode:
|
||||
return s.evalFieldNode(dot, n, nil, missingVal)
|
||||
case *parse.IdentifierNode:
|
||||
return s.evalFunction(dot, n, n, nil, missingVal)
|
||||
case *parse.NilNode:
|
||||
// NilNode is handled in evalArg, the only place that calls here.
|
||||
s.errorf("evalEmptyInterface: nil (can't happen)")
|
||||
case *parse.NumberNode:
|
||||
return s.idealConstant(n)
|
||||
case *parse.StringNode:
|
||||
return reflect.ValueOf(n.Text)
|
||||
case *parse.VariableNode:
|
||||
return s.evalVariableNode(dot, n, nil, missingVal)
|
||||
case *parse.PipeNode:
|
||||
return s.evalPipeline(dot, n)
|
||||
}
|
||||
s.errorf("can't handle assignment of %s to empty interface argument", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// indirect returns the item at the end of indirection, and a bool to indicate
|
||||
// if it's nil. If the returned bool is true, the returned value's kind will be
|
||||
// either a pointer or interface.
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// indirectInterface returns the concrete value in an interface value,
|
||||
// or else the zero reflect.Value.
|
||||
// That is, if v represents the interface value x, the result is the same as reflect.ValueOf(x):
|
||||
// the fact that x was an interface value is forgotten.
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
return v
|
||||
}
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
return v.Elem()
|
||||
}
|
||||
|
||||
// printValue writes the textual representation of the value to the output of
|
||||
// the template.
|
||||
func (s *state) printValue(n parse.Node, v reflect.Value) {
|
||||
s.at(n)
|
||||
iface, ok := printableValue(v)
|
||||
if !ok {
|
||||
s.errorf("can't print %s of type %s", n, v.Type())
|
||||
}
|
||||
_, err := fmt.Fprint(s.wr, iface)
|
||||
if err != nil {
|
||||
s.writeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// printableValue returns the, possibly indirected, interface value inside v that
|
||||
// is best for a call to formatted printer.
|
||||
func printableValue(v reflect.Value) (interface{}, bool) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v, _ = indirect(v) // fmt.Fprint handles nil.
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return "<no value>", true
|
||||
}
|
||||
|
||||
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
|
||||
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
|
||||
v = v.Addr()
|
||||
} else {
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return v.Interface(), true
|
||||
}
|
1624
tpl/internal/go_templates/texttemplate/exec_test.go
Normal file
1624
tpl/internal/go_templates/texttemplate/exec_test.go
Normal file
File diff suppressed because it is too large
Load diff
741
tpl/internal/go_templates/texttemplate/funcs.go
Normal file
741
tpl/internal/go_templates/texttemplate/funcs.go
Normal file
|
@ -0,0 +1,741 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FuncMap is the type of the map defining the mapping from names to functions.
|
||||
// Each function must have either a single return value, or two return values of
|
||||
// which the second has type error. In that case, if the second (error)
|
||||
// return value evaluates to non-nil during execution, execution terminates and
|
||||
// Execute returns that error.
|
||||
//
|
||||
// When template execution invokes a function with an argument list, that list
|
||||
// must be assignable to the function's parameter types. Functions meant to
|
||||
// apply to arguments of arbitrary type can use parameters of type interface{} or
|
||||
// of type reflect.Value. Similarly, functions meant to return a result of arbitrary
|
||||
// type can return interface{} or reflect.Value.
|
||||
type FuncMap map[string]interface{}
|
||||
|
||||
var builtins = FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
"html": HTMLEscaper,
|
||||
"index": index,
|
||||
"slice": slice,
|
||||
"js": JSEscaper,
|
||||
"len": length,
|
||||
"not": not,
|
||||
"or": or,
|
||||
"print": fmt.Sprint,
|
||||
"printf": fmt.Sprintf,
|
||||
"println": fmt.Sprintln,
|
||||
"urlquery": URLQueryEscaper,
|
||||
|
||||
// Comparisons
|
||||
"eq": eq, // ==
|
||||
"ge": ge, // >=
|
||||
"gt": gt, // >
|
||||
"le": le, // <=
|
||||
"lt": lt, // <
|
||||
"ne": ne, // !=
|
||||
}
|
||||
|
||||
var builtinFuncs = createValueFuncs(builtins)
|
||||
|
||||
// createValueFuncs turns a FuncMap into a map[string]reflect.Value
|
||||
func createValueFuncs(funcMap FuncMap) map[string]reflect.Value {
|
||||
m := make(map[string]reflect.Value)
|
||||
addValueFuncs(m, funcMap)
|
||||
return m
|
||||
}
|
||||
|
||||
// addValueFuncs adds to values the functions in funcs, converting them to reflect.Values.
|
||||
func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
|
||||
for name, fn := range in {
|
||||
if !goodName(name) {
|
||||
panic(fmt.Errorf("function name %q is not a valid identifier", name))
|
||||
}
|
||||
v := reflect.ValueOf(fn)
|
||||
if v.Kind() != reflect.Func {
|
||||
panic("value for " + name + " not a function")
|
||||
}
|
||||
if !goodFunc(v.Type()) {
|
||||
panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
|
||||
}
|
||||
out[name] = v
|
||||
}
|
||||
}
|
||||
|
||||
// addFuncs adds to values the functions in funcs. It does no checking of the input -
|
||||
// call addValueFuncs first.
|
||||
func addFuncs(out, in FuncMap) {
|
||||
for name, fn := range in {
|
||||
out[name] = fn
|
||||
}
|
||||
}
|
||||
|
||||
// goodFunc reports whether the function or method has the right result signature.
|
||||
func goodFunc(typ reflect.Type) bool {
|
||||
// We allow functions with 1 result or 2 results where the second is an error.
|
||||
switch {
|
||||
case typ.NumOut() == 1:
|
||||
return true
|
||||
case typ.NumOut() == 2 && typ.Out(1) == errorType:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// goodName reports whether the function name is a valid identifier.
|
||||
func goodName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
for i, r := range name {
|
||||
switch {
|
||||
case r == '_':
|
||||
case i == 0 && !unicode.IsLetter(r):
|
||||
return false
|
||||
case !unicode.IsLetter(r) && !unicode.IsDigit(r):
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// findFunction looks for a function in the template, and global map.
|
||||
func findFunction(name string, tmpl *Template) (reflect.Value, bool) {
|
||||
if tmpl != nil && tmpl.common != nil {
|
||||
tmpl.muFuncs.RLock()
|
||||
defer tmpl.muFuncs.RUnlock()
|
||||
if fn := tmpl.execFuncs[name]; fn.IsValid() {
|
||||
return fn, true
|
||||
}
|
||||
}
|
||||
if fn := builtinFuncs[name]; fn.IsValid() {
|
||||
return fn, true
|
||||
}
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
// prepareArg checks if value can be used as an argument of type argType, and
|
||||
// converts an invalid value to appropriate zero if possible.
|
||||
func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) {
|
||||
if !value.IsValid() {
|
||||
if !canBeNil(argType) {
|
||||
return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType)
|
||||
}
|
||||
value = reflect.Zero(argType)
|
||||
}
|
||||
if value.Type().AssignableTo(argType) {
|
||||
return value, nil
|
||||
}
|
||||
if intLike(value.Kind()) && intLike(argType.Kind()) && value.Type().ConvertibleTo(argType) {
|
||||
value = value.Convert(argType)
|
||||
return value, nil
|
||||
}
|
||||
return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType)
|
||||
}
|
||||
|
||||
func intLike(typ reflect.Kind) bool {
|
||||
switch typ {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// indexArg checks if a reflect.Value can be used as an index, and converts it to int if possible.
|
||||
func indexArg(index reflect.Value, cap int) (int, error) {
|
||||
var x int64
|
||||
switch index.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
x = index.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
x = int64(index.Uint())
|
||||
case reflect.Invalid:
|
||||
return 0, fmt.Errorf("cannot index slice/array with nil")
|
||||
default:
|
||||
return 0, fmt.Errorf("cannot index slice/array with type %s", index.Type())
|
||||
}
|
||||
if x < 0 || int(x) < 0 || int(x) > cap {
|
||||
return 0, fmt.Errorf("index out of range: %d", x)
|
||||
}
|
||||
return int(x), nil
|
||||
}
|
||||
|
||||
// Indexing.
|
||||
|
||||
// index returns the result of indexing its first argument by the following
|
||||
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
|
||||
// indexed item must be a map, slice, or array.
|
||||
func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
|
||||
v := indirectInterface(item)
|
||||
if !v.IsValid() {
|
||||
return reflect.Value{}, fmt.Errorf("index of untyped nil")
|
||||
}
|
||||
for _, i := range indexes {
|
||||
index := indirectInterface(i)
|
||||
var isNil bool
|
||||
if v, isNil = indirect(v); isNil {
|
||||
return reflect.Value{}, fmt.Errorf("index of nil pointer")
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
x, err := indexArg(index, v.Len())
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
v = v.Index(x)
|
||||
case reflect.Map:
|
||||
index, err := prepareArg(index, v.Type().Key())
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if x := v.MapIndex(index); x.IsValid() {
|
||||
v = x
|
||||
} else {
|
||||
v = reflect.Zero(v.Type().Elem())
|
||||
}
|
||||
case reflect.Invalid:
|
||||
// the loop holds invariant: v.IsValid()
|
||||
panic("unreachable")
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("can't index item of type %s", v.Type())
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Slicing.
|
||||
|
||||
// slice returns the result of slicing its first argument by the remaining
|
||||
// arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], while "slice x"
|
||||
// is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first
|
||||
// argument must be a string, slice, or array.
|
||||
func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
|
||||
var (
|
||||
cap int
|
||||
v = indirectInterface(item)
|
||||
)
|
||||
if !v.IsValid() {
|
||||
return reflect.Value{}, fmt.Errorf("slice of untyped nil")
|
||||
}
|
||||
if len(indexes) > 3 {
|
||||
return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
if len(indexes) == 3 {
|
||||
return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string")
|
||||
}
|
||||
cap = v.Len()
|
||||
case reflect.Array, reflect.Slice:
|
||||
cap = v.Cap()
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("can't slice item of type %s", v.Type())
|
||||
}
|
||||
// set default values for cases item[:], item[i:].
|
||||
idx := [3]int{0, v.Len()}
|
||||
for i, index := range indexes {
|
||||
x, err := indexArg(index, cap)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
idx[i] = x
|
||||
}
|
||||
// given item[i:j], make sure i <= j.
|
||||
if idx[0] > idx[1] {
|
||||
return reflect.Value{}, fmt.Errorf("invalid slice index: %d > %d", idx[0], idx[1])
|
||||
}
|
||||
if len(indexes) < 3 {
|
||||
return item.Slice(idx[0], idx[1]), nil
|
||||
}
|
||||
// given item[i:j:k], make sure i <= j <= k.
|
||||
if idx[1] > idx[2] {
|
||||
return reflect.Value{}, fmt.Errorf("invalid slice index: %d > %d", idx[1], idx[2])
|
||||
}
|
||||
return item.Slice3(idx[0], idx[1], idx[2]), nil
|
||||
}
|
||||
|
||||
// Length
|
||||
|
||||
// length returns the length of the item, with an error if it has no defined length.
|
||||
func length(item interface{}) (int, error) {
|
||||
v := reflect.ValueOf(item)
|
||||
if !v.IsValid() {
|
||||
return 0, fmt.Errorf("len of untyped nil")
|
||||
}
|
||||
v, isNil := indirect(v)
|
||||
if isNil {
|
||||
return 0, fmt.Errorf("len of nil pointer")
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len(), nil
|
||||
}
|
||||
return 0, fmt.Errorf("len of type %s", v.Type())
|
||||
}
|
||||
|
||||
// Function invocation
|
||||
|
||||
// call returns the result of evaluating the first argument as a function.
|
||||
// The function must return 1 result, or 2 results, the second of which is an error.
|
||||
func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
||||
v := indirectInterface(fn)
|
||||
if !v.IsValid() {
|
||||
return reflect.Value{}, fmt.Errorf("call of nil")
|
||||
}
|
||||
typ := v.Type()
|
||||
if typ.Kind() != reflect.Func {
|
||||
return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
return reflect.Value{}, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
|
||||
}
|
||||
numIn := typ.NumIn()
|
||||
var dddType reflect.Type
|
||||
if typ.IsVariadic() {
|
||||
if len(args) < numIn-1 {
|
||||
return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
|
||||
}
|
||||
dddType = typ.In(numIn - 1).Elem()
|
||||
} else {
|
||||
if len(args) != numIn {
|
||||
return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
|
||||
}
|
||||
}
|
||||
argv := make([]reflect.Value, len(args))
|
||||
for i, arg := range args {
|
||||
value := indirectInterface(arg)
|
||||
// Compute the expected type. Clumsy because of variadics.
|
||||
argType := dddType
|
||||
if !typ.IsVariadic() || i < numIn-1 {
|
||||
argType = typ.In(i)
|
||||
}
|
||||
|
||||
var err error
|
||||
if argv[i], err = prepareArg(value, argType); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("arg %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
return safeCall(v, argv)
|
||||
}
|
||||
|
||||
// safeCall runs fun.Call(args), and returns the resulting value and error, if
|
||||
// any. If the call panics, the panic value is returned as an error.
|
||||
func safeCall(fun reflect.Value, args []reflect.Value) (val reflect.Value, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if e, ok := r.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
ret := fun.Call(args)
|
||||
if len(ret) == 2 && !ret[1].IsNil() {
|
||||
return ret[0], ret[1].Interface().(error)
|
||||
}
|
||||
return ret[0], nil
|
||||
}
|
||||
|
||||
// Boolean logic.
|
||||
|
||||
func truth(arg reflect.Value) bool {
|
||||
t, _ := isTrue(indirectInterface(arg))
|
||||
return t
|
||||
}
|
||||
|
||||
// and computes the Boolean AND of its arguments, returning
|
||||
// the first false argument it encounters, or the last argument.
|
||||
func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
|
||||
if !truth(arg0) {
|
||||
return arg0
|
||||
}
|
||||
for i := range args {
|
||||
arg0 = args[i]
|
||||
if !truth(arg0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return arg0
|
||||
}
|
||||
|
||||
// or computes the Boolean OR of its arguments, returning
|
||||
// the first true argument it encounters, or the last argument.
|
||||
func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
|
||||
if truth(arg0) {
|
||||
return arg0
|
||||
}
|
||||
for i := range args {
|
||||
arg0 = args[i]
|
||||
if truth(arg0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return arg0
|
||||
}
|
||||
|
||||
// not returns the Boolean negation of its argument.
|
||||
func not(arg reflect.Value) bool {
|
||||
return !truth(arg)
|
||||
}
|
||||
|
||||
// Comparison.
|
||||
|
||||
// TODO: Perhaps allow comparison between signed and unsigned integers.
|
||||
|
||||
var (
|
||||
errBadComparisonType = errors.New("invalid type for comparison")
|
||||
errBadComparison = errors.New("incompatible types for comparison")
|
||||
errNoComparison = errors.New("missing argument for comparison")
|
||||
)
|
||||
|
||||
type kind int
|
||||
|
||||
const (
|
||||
invalidKind kind = iota
|
||||
boolKind
|
||||
complexKind
|
||||
intKind
|
||||
floatKind
|
||||
stringKind
|
||||
uintKind
|
||||
)
|
||||
|
||||
func basicKind(v reflect.Value) (kind, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return boolKind, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return intKind, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return uintKind, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatKind, nil
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return complexKind, nil
|
||||
case reflect.String:
|
||||
return stringKind, nil
|
||||
}
|
||||
return invalidKind, errBadComparisonType
|
||||
}
|
||||
|
||||
// eq evaluates the comparison a == b || a == c || ...
|
||||
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
|
||||
v1 := indirectInterface(arg1)
|
||||
k1, err := basicKind(v1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(arg2) == 0 {
|
||||
return false, errNoComparison
|
||||
}
|
||||
for _, arg := range arg2 {
|
||||
v2 := indirectInterface(arg)
|
||||
k2, err := basicKind(v2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
truth := false
|
||||
if k1 != k2 {
|
||||
// Special case: Can compare integer values regardless of type's sign.
|
||||
switch {
|
||||
case k1 == intKind && k2 == uintKind:
|
||||
truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
|
||||
case k1 == uintKind && k2 == intKind:
|
||||
truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
|
||||
default:
|
||||
return false, errBadComparison
|
||||
}
|
||||
} else {
|
||||
switch k1 {
|
||||
case boolKind:
|
||||
truth = v1.Bool() == v2.Bool()
|
||||
case complexKind:
|
||||
truth = v1.Complex() == v2.Complex()
|
||||
case floatKind:
|
||||
truth = v1.Float() == v2.Float()
|
||||
case intKind:
|
||||
truth = v1.Int() == v2.Int()
|
||||
case stringKind:
|
||||
truth = v1.String() == v2.String()
|
||||
case uintKind:
|
||||
truth = v1.Uint() == v2.Uint()
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
||||
if truth {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ne evaluates the comparison a != b.
|
||||
func ne(arg1, arg2 reflect.Value) (bool, error) {
|
||||
// != is the inverse of ==.
|
||||
equal, err := eq(arg1, arg2)
|
||||
return !equal, err
|
||||
}
|
||||
|
||||
// lt evaluates the comparison a < b.
|
||||
func lt(arg1, arg2 reflect.Value) (bool, error) {
|
||||
v1 := indirectInterface(arg1)
|
||||
k1, err := basicKind(v1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
v2 := indirectInterface(arg2)
|
||||
k2, err := basicKind(v2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
truth := false
|
||||
if k1 != k2 {
|
||||
// Special case: Can compare integer values regardless of type's sign.
|
||||
switch {
|
||||
case k1 == intKind && k2 == uintKind:
|
||||
truth = v1.Int() < 0 || uint64(v1.Int()) < v2.Uint()
|
||||
case k1 == uintKind && k2 == intKind:
|
||||
truth = v2.Int() >= 0 && v1.Uint() < uint64(v2.Int())
|
||||
default:
|
||||
return false, errBadComparison
|
||||
}
|
||||
} else {
|
||||
switch k1 {
|
||||
case boolKind, complexKind:
|
||||
return false, errBadComparisonType
|
||||
case floatKind:
|
||||
truth = v1.Float() < v2.Float()
|
||||
case intKind:
|
||||
truth = v1.Int() < v2.Int()
|
||||
case stringKind:
|
||||
truth = v1.String() < v2.String()
|
||||
case uintKind:
|
||||
truth = v1.Uint() < v2.Uint()
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
||||
return truth, nil
|
||||
}
|
||||
|
||||
// le evaluates the comparison <= b.
|
||||
func le(arg1, arg2 reflect.Value) (bool, error) {
|
||||
// <= is < or ==.
|
||||
lessThan, err := lt(arg1, arg2)
|
||||
if lessThan || err != nil {
|
||||
return lessThan, err
|
||||
}
|
||||
return eq(arg1, arg2)
|
||||
}
|
||||
|
||||
// gt evaluates the comparison a > b.
|
||||
func gt(arg1, arg2 reflect.Value) (bool, error) {
|
||||
// > is the inverse of <=.
|
||||
lessOrEqual, err := le(arg1, arg2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !lessOrEqual, nil
|
||||
}
|
||||
|
||||
// ge evaluates the comparison a >= b.
|
||||
func ge(arg1, arg2 reflect.Value) (bool, error) {
|
||||
// >= is the inverse of <.
|
||||
lessThan, err := lt(arg1, arg2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !lessThan, nil
|
||||
}
|
||||
|
||||
// HTML escaping.
|
||||
|
||||
var (
|
||||
htmlQuot = []byte(""") // shorter than """
|
||||
htmlApos = []byte("'") // shorter than "'" and apos was not in HTML until HTML5
|
||||
htmlAmp = []byte("&")
|
||||
htmlLt = []byte("<")
|
||||
htmlGt = []byte(">")
|
||||
htmlNull = []byte("\uFFFD")
|
||||
)
|
||||
|
||||
// HTMLEscape writes to w the escaped HTML equivalent of the plain text data b.
|
||||
func HTMLEscape(w io.Writer, b []byte) {
|
||||
last := 0
|
||||
for i, c := range b {
|
||||
var html []byte
|
||||
switch c {
|
||||
case '\000':
|
||||
html = htmlNull
|
||||
case '"':
|
||||
html = htmlQuot
|
||||
case '\'':
|
||||
html = htmlApos
|
||||
case '&':
|
||||
html = htmlAmp
|
||||
case '<':
|
||||
html = htmlLt
|
||||
case '>':
|
||||
html = htmlGt
|
||||
default:
|
||||
continue
|
||||
}
|
||||
w.Write(b[last:i])
|
||||
w.Write(html)
|
||||
last = i + 1
|
||||
}
|
||||
w.Write(b[last:])
|
||||
}
|
||||
|
||||
// HTMLEscapeString returns the escaped HTML equivalent of the plain text data s.
|
||||
func HTMLEscapeString(s string) string {
|
||||
// Avoid allocation if we can.
|
||||
if !strings.ContainsAny(s, "'\"&<>\000") {
|
||||
return s
|
||||
}
|
||||
var b bytes.Buffer
|
||||
HTMLEscape(&b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// HTMLEscaper returns the escaped HTML equivalent of the textual
|
||||
// representation of its arguments.
|
||||
func HTMLEscaper(args ...interface{}) string {
|
||||
return HTMLEscapeString(evalArgs(args))
|
||||
}
|
||||
|
||||
// JavaScript escaping.
|
||||
|
||||
var (
|
||||
jsLowUni = []byte(`\u00`)
|
||||
hex = []byte("0123456789ABCDEF")
|
||||
|
||||
jsBackslash = []byte(`\\`)
|
||||
jsApos = []byte(`\'`)
|
||||
jsQuot = []byte(`\"`)
|
||||
jsLt = []byte(`\x3C`)
|
||||
jsGt = []byte(`\x3E`)
|
||||
)
|
||||
|
||||
// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
|
||||
func JSEscape(w io.Writer, b []byte) {
|
||||
last := 0
|
||||
for i := 0; i < len(b); i++ {
|
||||
c := b[i]
|
||||
|
||||
if !jsIsSpecial(rune(c)) {
|
||||
// fast path: nothing to do
|
||||
continue
|
||||
}
|
||||
w.Write(b[last:i])
|
||||
|
||||
if c < utf8.RuneSelf {
|
||||
// Quotes, slashes and angle brackets get quoted.
|
||||
// Control characters get written as \u00XX.
|
||||
switch c {
|
||||
case '\\':
|
||||
w.Write(jsBackslash)
|
||||
case '\'':
|
||||
w.Write(jsApos)
|
||||
case '"':
|
||||
w.Write(jsQuot)
|
||||
case '<':
|
||||
w.Write(jsLt)
|
||||
case '>':
|
||||
w.Write(jsGt)
|
||||
default:
|
||||
w.Write(jsLowUni)
|
||||
t, b := c>>4, c&0x0f
|
||||
w.Write(hex[t : t+1])
|
||||
w.Write(hex[b : b+1])
|
||||
}
|
||||
} else {
|
||||
// Unicode rune.
|
||||
r, size := utf8.DecodeRune(b[i:])
|
||||
if unicode.IsPrint(r) {
|
||||
w.Write(b[i : i+size])
|
||||
} else {
|
||||
fmt.Fprintf(w, "\\u%04X", r)
|
||||
}
|
||||
i += size - 1
|
||||
}
|
||||
last = i + 1
|
||||
}
|
||||
w.Write(b[last:])
|
||||
}
|
||||
|
||||
// JSEscapeString returns the escaped JavaScript equivalent of the plain text data s.
|
||||
func JSEscapeString(s string) string {
|
||||
// Avoid allocation if we can.
|
||||
if strings.IndexFunc(s, jsIsSpecial) < 0 {
|
||||
return s
|
||||
}
|
||||
var b bytes.Buffer
|
||||
JSEscape(&b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func jsIsSpecial(r rune) bool {
|
||||
switch r {
|
||||
case '\\', '\'', '"', '<', '>':
|
||||
return true
|
||||
}
|
||||
return r < ' ' || utf8.RuneSelf <= r
|
||||
}
|
||||
|
||||
// JSEscaper returns the escaped JavaScript equivalent of the textual
|
||||
// representation of its arguments.
|
||||
func JSEscaper(args ...interface{}) string {
|
||||
return JSEscapeString(evalArgs(args))
|
||||
}
|
||||
|
||||
// URLQueryEscaper returns the escaped value of the textual representation of
|
||||
// its arguments in a form suitable for embedding in a URL query.
|
||||
func URLQueryEscaper(args ...interface{}) string {
|
||||
return url.QueryEscape(evalArgs(args))
|
||||
}
|
||||
|
||||
// evalArgs formats the list of arguments into a string. It is therefore equivalent to
|
||||
// fmt.Sprint(args...)
|
||||
// except that each argument is indirected (if a pointer), as required,
|
||||
// using the same rules as the default string evaluation during template
|
||||
// execution.
|
||||
func evalArgs(args []interface{}) string {
|
||||
ok := false
|
||||
var s string
|
||||
// Fast path for simple common case.
|
||||
if len(args) == 1 {
|
||||
s, ok = args[0].(string)
|
||||
}
|
||||
if !ok {
|
||||
for i, arg := range args {
|
||||
a, ok := printableValue(reflect.ValueOf(arg))
|
||||
if ok {
|
||||
args[i] = a
|
||||
} // else let fmt do its thing
|
||||
}
|
||||
s = fmt.Sprint(args...)
|
||||
}
|
||||
return s
|
||||
}
|
130
tpl/internal/go_templates/texttemplate/helper.go
Normal file
130
tpl/internal/go_templates/texttemplate/helper.go
Normal file
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// Helper functions to make constructing templates easier.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Functions and methods to parse templates.
|
||||
|
||||
// Must is a helper that wraps a call to a function returning (*Template, error)
|
||||
// and panics if the error is non-nil. It is intended for use in variable
|
||||
// initializations such as
|
||||
// var t = template.Must(template.New("name").Parse("text"))
|
||||
func Must(t *Template, err error) *Template {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ParseFiles creates a new Template and parses the template definitions from
|
||||
// the named files. The returned template's name will have the base name and
|
||||
// parsed contents of the first file. There must be at least one file.
|
||||
// If an error occurs, parsing stops and the returned *Template is nil.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template
|
||||
// named "foo", while "a/foo" is unavailable.
|
||||
func ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(nil, filenames...)
|
||||
}
|
||||
|
||||
// ParseFiles parses the named files and associates the resulting templates with
|
||||
// t. If an error occurs, parsing stops and the returned template is nil;
|
||||
// otherwise it is t. There must be at least one file.
|
||||
// Since the templates created by ParseFiles are named by the base
|
||||
// names of the argument files, t should usually have the name of one
|
||||
// of the (base) names of the files. If it does not, depending on t's
|
||||
// contents before calling ParseFiles, t.Execute may fail. In that
|
||||
// case use t.ExecuteTemplate to execute a valid template.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
|
||||
t.init()
|
||||
return parseFiles(t, filenames...)
|
||||
}
|
||||
|
||||
// parseFiles is the helper for the method and function. If the argument
|
||||
// template is nil, it is created from the first file.
|
||||
func parseFiles(t *Template, filenames ...string) (*Template, error) {
|
||||
if len(filenames) == 0 {
|
||||
// Not really a problem, but be consistent.
|
||||
return nil, fmt.Errorf("template: no files named in call to ParseFiles")
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(b)
|
||||
name := filepath.Base(filename)
|
||||
// First template becomes return value if not already defined,
|
||||
// and we use that one for subsequent New calls to associate
|
||||
// all the templates together. Also, if this file has the same name
|
||||
// as t, this file becomes the contents of t, so
|
||||
// t, err := New(name).Funcs(xxx).ParseFiles(name)
|
||||
// works. Otherwise we create a new template associated with t.
|
||||
var tmpl *Template
|
||||
if t == nil {
|
||||
t = New(name)
|
||||
}
|
||||
if name == t.Name() {
|
||||
tmpl = t
|
||||
} else {
|
||||
tmpl = t.New(name)
|
||||
}
|
||||
_, err = tmpl.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ParseGlob creates a new Template and parses the template definitions from
|
||||
// the files identified by the pattern. The files are matched according to the
|
||||
// semantics of filepath.Match, and the pattern must match at least one file.
|
||||
// The returned template will have the (base) name and (parsed) contents of the
|
||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
||||
// ParseFiles with the list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
func ParseGlob(pattern string) (*Template, error) {
|
||||
return parseGlob(nil, pattern)
|
||||
}
|
||||
|
||||
// ParseGlob parses the template definitions in the files identified by the
|
||||
// pattern and associates the resulting templates with t. The files are matched
|
||||
// according to the semantics of filepath.Match, and the pattern must match at
|
||||
// least one file. ParseGlob is equivalent to calling t.ParseFiles with the
|
||||
// list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
func (t *Template) ParseGlob(pattern string) (*Template, error) {
|
||||
t.init()
|
||||
return parseGlob(t, pattern)
|
||||
}
|
||||
|
||||
// parseGlob is the implementation of the function and method ParseGlob.
|
||||
func parseGlob(t *Template, pattern string) (*Template, error) {
|
||||
filenames, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
return parseFiles(t, filenames...)
|
||||
}
|
38
tpl/internal/go_templates/texttemplate/hugo_exec.go
Normal file
38
tpl/internal/go_templates/texttemplate/hugo_exec.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
This files contains the Hugo related addons. All the other files in this
|
||||
package is auto generated.
|
||||
|
||||
*/
|
||||
|
||||
// state represents the state of an execution. It's not part of the
|
||||
// template so that multiple executions of the same template
|
||||
// can execute in parallel.
|
||||
type state struct {
|
||||
tmpl *Template
|
||||
wr io.Writer
|
||||
node parse.Node // current node, for errors
|
||||
vars []variable // push-down stack of variable values.
|
||||
depth int // the height of the stack of executing templates.
|
||||
}
|
85
tpl/internal/go_templates/texttemplate/hugo_template.go
Normal file
85
tpl/internal/go_templates/texttemplate/hugo_template.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
This files contains the Hugo related addons. All the other files in this
|
||||
package is auto generated.
|
||||
|
||||
*/
|
||||
|
||||
// TODO1 name
|
||||
type Preparer interface {
|
||||
Prepare() (*Template, error)
|
||||
}
|
||||
|
||||
type TemplateExecutor struct {
|
||||
}
|
||||
|
||||
func (t *TemplateExecutor) Execute(p Preparer, wr io.Writer, data interface{}) error {
|
||||
tmpl, err := p.Prepare()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, ok := data.(reflect.Value)
|
||||
if !ok {
|
||||
value = reflect.ValueOf(data)
|
||||
}
|
||||
|
||||
state := &state{
|
||||
tmpl: tmpl,
|
||||
wr: wr,
|
||||
vars: []variable{{"$", value}},
|
||||
}
|
||||
|
||||
return tmpl.executeWithState(state, value)
|
||||
|
||||
}
|
||||
|
||||
// Prepare returns a template ready for execution.
|
||||
func (t *Template) Prepare() (*Template, error) {
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Below are modifed structs etc.
|
||||
|
||||
// state represents the state of an execution. It's not part of the
|
||||
// template so that multiple executions of the same template
|
||||
// can execute in parallel.
|
||||
type state struct {
|
||||
tmpl *Template
|
||||
wr io.Writer
|
||||
node parse.Node // current node, for errors
|
||||
vars []variable // push-down stack of variable values.
|
||||
depth int // the height of the stack of executing templates.
|
||||
}
|
||||
|
||||
func (t *Template) executeWithState(state *state, value reflect.Value) (err error) {
|
||||
defer errRecover(&err)
|
||||
if t.Tree == nil || t.Root == nil {
|
||||
state.errorf("%q is an incomplete or empty template", t.Name())
|
||||
}
|
||||
state.walk(value, t.Root)
|
||||
return
|
||||
}
|
425
tpl/internal/go_templates/texttemplate/multi_test.go
Normal file
425
tpl/internal/go_templates/texttemplate/multi_test.go
Normal file
|
@ -0,0 +1,425 @@
|
|||
// Copyright 2011 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 go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
// Tests for multiple-template parsing and execution.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
type multiParseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
names []string
|
||||
results []string
|
||||
}
|
||||
|
||||
var multiParseTests = []multiParseTest{
|
||||
{"empty", "", noError,
|
||||
nil,
|
||||
nil},
|
||||
{"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
[]string{"foo"},
|
||||
[]string{" FOO "}},
|
||||
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{"foo", "bar"},
|
||||
[]string{" FOO ", " BAR "}},
|
||||
// errors
|
||||
{"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
{"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
}
|
||||
|
||||
func TestMultiParse(t *testing.T) {
|
||||
for _, test := range multiParseTests {
|
||||
template, err := New("root").Parse(test.input)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if template == nil {
|
||||
continue
|
||||
}
|
||||
if len(template.tmpl) != len(test.names)+1 { // +1 for root
|
||||
t.Errorf("%s: wrong number of templates; wanted %d got %d", test.name, len(test.names), len(template.tmpl))
|
||||
continue
|
||||
}
|
||||
for i, name := range test.names {
|
||||
tmpl, ok := template.tmpl[name]
|
||||
if !ok {
|
||||
t.Errorf("%s: can't find template %q", test.name, name)
|
||||
continue
|
||||
}
|
||||
result := tmpl.Root.String()
|
||||
if result != test.results[i] {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var multiExecTests = []execTest{
|
||||
{"empty", "", "", nil, true},
|
||||
{"text", "some text", "some text", nil, true},
|
||||
{"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true},
|
||||
{"invoke x no args", `{{template "x"}}`, "TEXT", tVal, true},
|
||||
{"invoke dot int", `{{template "dot" .I}}`, "17", tVal, true},
|
||||
{"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true},
|
||||
{"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true},
|
||||
{"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true},
|
||||
{"variable declared by template", `{{template "nested" $x:=.SI}},{{index $x 1}}`, "[3 4 5],4", tVal, true},
|
||||
|
||||
// User-defined function: test argument evaluator.
|
||||
{"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true},
|
||||
{"testFunc .", `{{oneArg .}}`, "oneArg=joe", "joe", true},
|
||||
}
|
||||
|
||||
// These strings are also in testdata/*.
|
||||
const multiText1 = `
|
||||
{{define "x"}}TEXT{{end}}
|
||||
{{define "dotV"}}{{.V}}{{end}}
|
||||
`
|
||||
|
||||
const multiText2 = `
|
||||
{{define "dot"}}{{.}}{{end}}
|
||||
{{define "nested"}}{{template "dot" .}}{{end}}
|
||||
`
|
||||
|
||||
func TestMultiExecute(t *testing.T) {
|
||||
// Declare a couple of templates first.
|
||||
template, err := New("root").Parse(multiText1)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 1: %s", err)
|
||||
}
|
||||
_, err = template.Parse(multiText2)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 2: %s", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFiles(t *testing.T) {
|
||||
_, err := ParseFiles("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlob(t *testing.T) {
|
||||
_, err := ParseGlob("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
_, err = New("error").ParseGlob("[x")
|
||||
if err == nil {
|
||||
t.Error("expected error for bad pattern; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseGlob("testdata/file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
// In these tests, actual content (not just template definitions) comes from the parsed files.
|
||||
|
||||
var templateFileExecTests = []execTest{
|
||||
{"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\n\ny\ntemplate2\n\nx\n", 0, true},
|
||||
}
|
||||
|
||||
func TestParseFilesWithData(t *testing.T) {
|
||||
template, err := New("root").ParseFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlobWithData(t *testing.T) {
|
||||
template, err := New("root").ParseGlob("testdata/tmpl*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
const (
|
||||
cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}`
|
||||
cloneText2 = `{{define "b"}}b{{end}}`
|
||||
cloneText3 = `{{define "c"}}root{{end}}`
|
||||
cloneText4 = `{{define "c"}}clone{{end}}`
|
||||
)
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
// Create some templates and clone the root.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clone := Must(root.Clone())
|
||||
// Add variants to both.
|
||||
_, err = root.Parse(cloneText3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = clone.Parse(cloneText4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Verify that the clone is self-consistent.
|
||||
for k, v := range clone.tmpl {
|
||||
if k == clone.name && v.tmpl[k] != clone {
|
||||
t.Error("clone does not contain root")
|
||||
}
|
||||
if v != v.tmpl[v.name] {
|
||||
t.Errorf("clone does not contain self for %q", k)
|
||||
}
|
||||
}
|
||||
// Execute root.
|
||||
var b bytes.Buffer
|
||||
err = root.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
// Execute copy.
|
||||
b.Reset()
|
||||
err = clone.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "bclone" {
|
||||
t.Errorf("expected %q got %q", "bclone", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddParseTree(t *testing.T) {
|
||||
// Create some templates.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add a new parse tree.
|
||||
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added, err := root.AddParseTree("c", tree["c"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Execute.
|
||||
var b bytes.Buffer
|
||||
err = added.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 7032
|
||||
func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
|
||||
master := "{{define \"master\"}}{{end}}"
|
||||
tmpl := New("master")
|
||||
tree, err := parse.Parse("master", master, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse err: %v", err)
|
||||
}
|
||||
masterTree := tree["master"]
|
||||
tmpl.AddParseTree("master", masterTree) // used to panic
|
||||
}
|
||||
|
||||
func TestRedefinition(t *testing.T) {
|
||||
var tmpl *Template
|
||||
var err error
|
||||
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parse 1: %v", err)
|
||||
}
|
||||
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err != nil {
|
||||
t.Fatalf("got error %v, expected nil", err)
|
||||
}
|
||||
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err != nil {
|
||||
t.Fatalf("got error %v, expected nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 10879
|
||||
func TestEmptyTemplateCloneCrash(t *testing.T) {
|
||||
t1 := New("base")
|
||||
t1.Clone() // used to panic
|
||||
}
|
||||
|
||||
// Issue 10910, 10926
|
||||
func TestTemplateLookUp(t *testing.T) {
|
||||
t1 := New("foo")
|
||||
if t1.Lookup("foo") != nil {
|
||||
t.Error("Lookup returned non-nil value for undefined template foo")
|
||||
}
|
||||
t1.New("bar")
|
||||
if t1.Lookup("bar") != nil {
|
||||
t.Error("Lookup returned non-nil value for undefined template bar")
|
||||
}
|
||||
t1.Parse(`{{define "foo"}}test{{end}}`)
|
||||
if t1.Lookup("foo") == nil {
|
||||
t.Error("Lookup returned nil value for defined template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
// template with same name already exists
|
||||
t1, _ := New("test").Parse(`{{define "test"}}foo{{end}}`)
|
||||
t2 := t1.New("test")
|
||||
|
||||
if t1.common != t2.common {
|
||||
t.Errorf("t1 & t2 didn't share common struct; got %v != %v", t1.common, t2.common)
|
||||
}
|
||||
if t1.Tree == nil {
|
||||
t.Error("defined template got nil Tree")
|
||||
}
|
||||
if t2.Tree != nil {
|
||||
t.Error("undefined template got non-nil Tree")
|
||||
}
|
||||
|
||||
containsT1 := false
|
||||
for _, tmpl := range t1.Templates() {
|
||||
if tmpl == t2 {
|
||||
t.Error("Templates included undefined template")
|
||||
}
|
||||
if tmpl == t1 {
|
||||
containsT1 = true
|
||||
}
|
||||
}
|
||||
if !containsT1 {
|
||||
t.Error("Templates didn't include defined template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
// In multiple calls to Parse with the same receiver template, only one call
|
||||
// can contain text other than space, comments, and template definitions
|
||||
t1 := New("test")
|
||||
if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil {
|
||||
t.Fatalf("parsing test: %s", err)
|
||||
}
|
||||
if _, err := t1.Parse(`{{define "test"}}{{/* this is a comment */}}{{end}}`); err != nil {
|
||||
t.Fatalf("parsing test: %s", err)
|
||||
}
|
||||
if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parsing test: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyTemplate(t *testing.T) {
|
||||
cases := []struct {
|
||||
defn []string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{[]string{""}, "once", ""},
|
||||
{[]string{"", ""}, "twice", ""},
|
||||
{[]string{"{{.}}", "{{.}}"}, "twice", "twice"},
|
||||
{[]string{"{{/* a comment */}}", "{{/* a comment */}}"}, "comment", ""},
|
||||
{[]string{"{{.}}", ""}, "twice", ""},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
root := New("root")
|
||||
|
||||
var (
|
||||
m *Template
|
||||
err error
|
||||
)
|
||||
for _, d := range c.defn {
|
||||
m, err = root.New(c.in).Parse(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := m.Execute(buf, c.in); err != nil {
|
||||
t.Error(i, err)
|
||||
continue
|
||||
}
|
||||
if buf.String() != c.want {
|
||||
t.Errorf("expected string %q: got %q", c.want, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 19249 was a regression in 1.8 caused by the handling of empty
|
||||
// templates added in that release, which got different answers depending
|
||||
// on the order templates appeared in the internal map.
|
||||
func TestIssue19294(t *testing.T) {
|
||||
// The empty block in "xhtml" should be replaced during execution
|
||||
// by the contents of "stylesheet", but if the internal map associating
|
||||
// names with templates is built in the wrong order, the empty block
|
||||
// looks non-empty and this doesn't happen.
|
||||
var inlined = map[string]string{
|
||||
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
|
||||
"xhtml": `{{block "stylesheet" .}}{{end}}`,
|
||||
}
|
||||
all := []string{"stylesheet", "xhtml"}
|
||||
for i := 0; i < 100; i++ {
|
||||
res, err := New("title.xhtml").Parse(`{{template "xhtml" .}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range all {
|
||||
_, err := res.New(name).Parse(inlined[name])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
res.Execute(&buf, 0)
|
||||
if buf.String() != "stylesheet" {
|
||||
t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")
|
||||
}
|
||||
}
|
||||
}
|
74
tpl/internal/go_templates/texttemplate/option.go
Normal file
74
tpl/internal/go_templates/texttemplate/option.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
// 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.
|
||||
|
||||
// This file contains the code to handle template options.
|
||||
|
||||
package template
|
||||
|
||||
import "strings"
|
||||
|
||||
// missingKeyAction defines how to respond to indexing a map with a key that is not present.
|
||||
type missingKeyAction int
|
||||
|
||||
const (
|
||||
mapInvalid missingKeyAction = iota // Return an invalid reflect.Value.
|
||||
mapZeroValue // Return the zero value for the map element.
|
||||
mapError // Error out
|
||||
)
|
||||
|
||||
type option struct {
|
||||
missingKey missingKeyAction
|
||||
}
|
||||
|
||||
// Option sets options for the template. Options are described by
|
||||
// strings, either a simple string or "key=value". There can be at
|
||||
// most one equals sign in an option string. If the option string
|
||||
// is unrecognized or otherwise invalid, Option panics.
|
||||
//
|
||||
// Known options:
|
||||
//
|
||||
// missingkey: Control the behavior during execution if a map is
|
||||
// indexed with a key that is not present in the map.
|
||||
// "missingkey=default" or "missingkey=invalid"
|
||||
// The default behavior: Do nothing and continue execution.
|
||||
// If printed, the result of the index operation is the string
|
||||
// "<no value>".
|
||||
// "missingkey=zero"
|
||||
// The operation returns the zero value for the map type's element.
|
||||
// "missingkey=error"
|
||||
// Execution stops immediately with an error.
|
||||
//
|
||||
func (t *Template) Option(opt ...string) *Template {
|
||||
t.init()
|
||||
for _, s := range opt {
|
||||
t.setOption(s)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Template) setOption(opt string) {
|
||||
if opt == "" {
|
||||
panic("empty option string")
|
||||
}
|
||||
elems := strings.Split(opt, "=")
|
||||
switch len(elems) {
|
||||
case 2:
|
||||
// key=value
|
||||
switch elems[0] {
|
||||
case "missingkey":
|
||||
switch elems[1] {
|
||||
case "invalid", "default":
|
||||
t.option.missingKey = mapInvalid
|
||||
return
|
||||
case "zero":
|
||||
t.option.missingKey = mapZeroValue
|
||||
return
|
||||
case "error":
|
||||
t.option.missingKey = mapError
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
panic("unrecognized option: " + opt)
|
||||
}
|
666
tpl/internal/go_templates/texttemplate/parse/lex.go
Normal file
666
tpl/internal/go_templates/texttemplate/parse/lex.go
Normal file
|
@ -0,0 +1,666 @@
|
|||
// Copyright 2011 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 parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// item represents a token or text string returned from the scanner.
|
||||
type item struct {
|
||||
typ itemType // The type of this item.
|
||||
pos Pos // The starting position, in bytes, of this item in the input string.
|
||||
val string // The value of this item.
|
||||
line int // The line number at the start of this item.
|
||||
}
|
||||
|
||||
func (i item) String() string {
|
||||
switch {
|
||||
case i.typ == itemEOF:
|
||||
return "EOF"
|
||||
case i.typ == itemError:
|
||||
return i.val
|
||||
case i.typ > itemKeyword:
|
||||
return fmt.Sprintf("<%s>", i.val)
|
||||
case len(i.val) > 10:
|
||||
return fmt.Sprintf("%.10q...", i.val)
|
||||
}
|
||||
return fmt.Sprintf("%q", i.val)
|
||||
}
|
||||
|
||||
// itemType identifies the type of lex items.
|
||||
type itemType int
|
||||
|
||||
const (
|
||||
itemError itemType = iota // error occurred; value is text of error
|
||||
itemBool // boolean constant
|
||||
itemChar // printable ASCII character; grab bag for comma etc.
|
||||
itemCharConstant // character constant
|
||||
itemComplex // complex constant (1+2i); imaginary is just a number
|
||||
itemAssign // equals ('=') introducing an assignment
|
||||
itemDeclare // colon-equals (':=') introducing a declaration
|
||||
itemEOF
|
||||
itemField // alphanumeric identifier starting with '.'
|
||||
itemIdentifier // alphanumeric identifier not starting with '.'
|
||||
itemLeftDelim // left action delimiter
|
||||
itemLeftParen // '(' inside action
|
||||
itemNumber // simple number, including imaginary
|
||||
itemPipe // pipe symbol
|
||||
itemRawString // raw quoted string (includes quotes)
|
||||
itemRightDelim // right action delimiter
|
||||
itemRightParen // ')' inside action
|
||||
itemSpace // run of spaces separating arguments
|
||||
itemString // quoted string (includes quotes)
|
||||
itemText // plain text
|
||||
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'
|
||||
// Keywords appear after all the rest.
|
||||
itemKeyword // used only to delimit the keywords
|
||||
itemBlock // block keyword
|
||||
itemDot // the cursor, spelled '.'
|
||||
itemDefine // define keyword
|
||||
itemElse // else keyword
|
||||
itemEnd // end keyword
|
||||
itemIf // if keyword
|
||||
itemNil // the untyped nil constant, easiest to treat as a keyword
|
||||
itemRange // range keyword
|
||||
itemTemplate // template keyword
|
||||
itemWith // with keyword
|
||||
)
|
||||
|
||||
var key = map[string]itemType{
|
||||
".": itemDot,
|
||||
"block": itemBlock,
|
||||
"define": itemDefine,
|
||||
"else": itemElse,
|
||||
"end": itemEnd,
|
||||
"if": itemIf,
|
||||
"range": itemRange,
|
||||
"nil": itemNil,
|
||||
"template": itemTemplate,
|
||||
"with": itemWith,
|
||||
}
|
||||
|
||||
const eof = -1
|
||||
|
||||
// Trimming spaces.
|
||||
// If the action begins "{{- " rather than "{{", then all space/tab/newlines
|
||||
// preceding the action are trimmed; conversely if it ends " -}}" the
|
||||
// leading spaces are trimmed. This is done entirely in the lexer; the
|
||||
// parser never sees it happen. We require an ASCII space to be
|
||||
// present to avoid ambiguity with things like "{{-3}}". It reads
|
||||
// better with the space present anyway. For simplicity, only ASCII
|
||||
// space does the job.
|
||||
const (
|
||||
spaceChars = " \t\r\n" // These are the space characters defined by Go itself.
|
||||
leftTrimMarker = "- " // Attached to left delimiter, trims trailing spaces from preceding text.
|
||||
rightTrimMarker = " -" // Attached to right delimiter, trims leading spaces from following text.
|
||||
trimMarkerLen = Pos(len(leftTrimMarker))
|
||||
)
|
||||
|
||||
// stateFn represents the state of the scanner as a function that returns the next state.
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
// lexer holds the state of the scanner.
|
||||
type lexer struct {
|
||||
name string // the name of the input; used only for error reports
|
||||
input string // the string being scanned
|
||||
leftDelim string // start of action
|
||||
rightDelim string // end of action
|
||||
trimRightDelim string // end of action with trim marker
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
items chan item // channel of scanned items
|
||||
parenDepth int // nesting depth of ( ) exprs
|
||||
line int // 1+number of newlines seen
|
||||
startLine int // start line of this item
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
func (l *lexer) next() rune {
|
||||
if int(l.pos) >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = Pos(w)
|
||||
l.pos += l.width
|
||||
if r == '\n' {
|
||||
l.line++
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next rune in the input.
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per call of next.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
// Correct newline count.
|
||||
if l.width == 1 && l.input[l.pos] == '\n' {
|
||||
l.line--
|
||||
}
|
||||
}
|
||||
|
||||
// emit passes an item back to the client.
|
||||
func (l *lexer) emit(t itemType) {
|
||||
l.items <- item{t, l.start, l.input[l.start:l.pos], l.startLine}
|
||||
l.start = l.pos
|
||||
l.startLine = l.line
|
||||
}
|
||||
|
||||
// ignore skips over the pending input before this point.
|
||||
func (l *lexer) ignore() {
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
l.start = l.pos
|
||||
l.startLine = l.line
|
||||
}
|
||||
|
||||
// accept consumes the next rune if it's from the valid set.
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.ContainsRune(valid, l.next()) {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
// acceptRun consumes a run of runes from the valid set.
|
||||
func (l *lexer) acceptRun(valid string) {
|
||||
for strings.ContainsRune(valid, l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
}
|
||||
|
||||
// errorf returns an error token and terminates the scan by passing
|
||||
// back a nil pointer that will be the next state, terminating l.nextItem.
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextItem returns the next item from the input.
|
||||
// Called by the parser, not in the lexing goroutine.
|
||||
func (l *lexer) nextItem() item {
|
||||
return <-l.items
|
||||
}
|
||||
|
||||
// drain drains the output so the lexing goroutine will exit.
|
||||
// Called by the parser, not in the lexing goroutine.
|
||||
func (l *lexer) drain() {
|
||||
for range l.items {
|
||||
}
|
||||
}
|
||||
|
||||
// lex creates a new scanner for the input string.
|
||||
func lex(name, input, left, right string) *lexer {
|
||||
if left == "" {
|
||||
left = leftDelim
|
||||
}
|
||||
if right == "" {
|
||||
right = rightDelim
|
||||
}
|
||||
l := &lexer{
|
||||
name: name,
|
||||
input: input,
|
||||
leftDelim: left,
|
||||
rightDelim: right,
|
||||
trimRightDelim: rightTrimMarker + right,
|
||||
items: make(chan item),
|
||||
line: 1,
|
||||
startLine: 1,
|
||||
}
|
||||
go l.run()
|
||||
return l
|
||||
}
|
||||
|
||||
// run runs the state machine for the lexer.
|
||||
func (l *lexer) run() {
|
||||
for state := lexText; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
close(l.items)
|
||||
}
|
||||
|
||||
// state functions
|
||||
|
||||
const (
|
||||
leftDelim = "{{"
|
||||
rightDelim = "}}"
|
||||
leftComment = "/*"
|
||||
rightComment = "*/"
|
||||
)
|
||||
|
||||
// lexText scans until an opening action delimiter, "{{".
|
||||
func lexText(l *lexer) stateFn {
|
||||
l.width = 0
|
||||
if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
|
||||
ldn := Pos(len(l.leftDelim))
|
||||
l.pos += Pos(x)
|
||||
trimLength := Pos(0)
|
||||
if strings.HasPrefix(l.input[l.pos+ldn:], leftTrimMarker) {
|
||||
trimLength = rightTrimLength(l.input[l.start:l.pos])
|
||||
}
|
||||
l.pos -= trimLength
|
||||
if l.pos > l.start {
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
l.emit(itemText)
|
||||
}
|
||||
l.pos += trimLength
|
||||
l.ignore()
|
||||
return lexLeftDelim
|
||||
}
|
||||
l.pos = Pos(len(l.input))
|
||||
// Correctly reached EOF.
|
||||
if l.pos > l.start {
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
l.emit(itemText)
|
||||
}
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rightTrimLength returns the length of the spaces at the end of the string.
|
||||
func rightTrimLength(s string) Pos {
|
||||
return Pos(len(s) - len(strings.TrimRight(s, spaceChars)))
|
||||
}
|
||||
|
||||
// atRightDelim reports whether the lexer is at a right delimiter, possibly preceded by a trim marker.
|
||||
func (l *lexer) atRightDelim() (delim, trimSpaces bool) {
|
||||
if strings.HasPrefix(l.input[l.pos:], l.trimRightDelim) { // With trim marker.
|
||||
return true, true
|
||||
}
|
||||
if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { // Without trim marker.
|
||||
return true, false
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// leftTrimLength returns the length of the spaces at the beginning of the string.
|
||||
func leftTrimLength(s string) Pos {
|
||||
return Pos(len(s) - len(strings.TrimLeft(s, spaceChars)))
|
||||
}
|
||||
|
||||
// lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker.
|
||||
func lexLeftDelim(l *lexer) stateFn {
|
||||
l.pos += Pos(len(l.leftDelim))
|
||||
trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker)
|
||||
afterMarker := Pos(0)
|
||||
if trimSpace {
|
||||
afterMarker = trimMarkerLen
|
||||
}
|
||||
if strings.HasPrefix(l.input[l.pos+afterMarker:], leftComment) {
|
||||
l.pos += afterMarker
|
||||
l.ignore()
|
||||
return lexComment
|
||||
}
|
||||
l.emit(itemLeftDelim)
|
||||
l.pos += afterMarker
|
||||
l.ignore()
|
||||
l.parenDepth = 0
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexComment scans a comment. The left comment marker is known to be present.
|
||||
func lexComment(l *lexer) stateFn {
|
||||
l.pos += Pos(len(leftComment))
|
||||
i := strings.Index(l.input[l.pos:], rightComment)
|
||||
if i < 0 {
|
||||
return l.errorf("unclosed comment")
|
||||
}
|
||||
l.pos += Pos(i + len(rightComment))
|
||||
delim, trimSpace := l.atRightDelim()
|
||||
if !delim {
|
||||
return l.errorf("comment ends before closing delimiter")
|
||||
}
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
}
|
||||
l.pos += Pos(len(l.rightDelim))
|
||||
if trimSpace {
|
||||
l.pos += leftTrimLength(l.input[l.pos:])
|
||||
}
|
||||
l.ignore()
|
||||
return lexText
|
||||
}
|
||||
|
||||
// lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker.
|
||||
func lexRightDelim(l *lexer) stateFn {
|
||||
trimSpace := strings.HasPrefix(l.input[l.pos:], rightTrimMarker)
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
l.ignore()
|
||||
}
|
||||
l.pos += Pos(len(l.rightDelim))
|
||||
l.emit(itemRightDelim)
|
||||
if trimSpace {
|
||||
l.pos += leftTrimLength(l.input[l.pos:])
|
||||
l.ignore()
|
||||
}
|
||||
return lexText
|
||||
}
|
||||
|
||||
// lexInsideAction scans the elements inside action delimiters.
|
||||
func lexInsideAction(l *lexer) stateFn {
|
||||
// Either number, quoted string, or identifier.
|
||||
// Spaces separate arguments; runs of spaces turn into itemSpace.
|
||||
// Pipe symbols separate and are emitted.
|
||||
delim, _ := l.atRightDelim()
|
||||
if delim {
|
||||
if l.parenDepth == 0 {
|
||||
return lexRightDelim
|
||||
}
|
||||
return l.errorf("unclosed left paren")
|
||||
}
|
||||
switch r := l.next(); {
|
||||
case r == eof || isEndOfLine(r):
|
||||
return l.errorf("unclosed action")
|
||||
case isSpace(r):
|
||||
l.backup() // Put space back in case we have " -}}".
|
||||
return lexSpace
|
||||
case r == '=':
|
||||
l.emit(itemAssign)
|
||||
case r == ':':
|
||||
if l.next() != '=' {
|
||||
return l.errorf("expected :=")
|
||||
}
|
||||
l.emit(itemDeclare)
|
||||
case r == '|':
|
||||
l.emit(itemPipe)
|
||||
case r == '"':
|
||||
return lexQuote
|
||||
case r == '`':
|
||||
return lexRawQuote
|
||||
case r == '$':
|
||||
return lexVariable
|
||||
case r == '\'':
|
||||
return lexChar
|
||||
case r == '.':
|
||||
// special look-ahead for ".field" so we don't break l.backup().
|
||||
if l.pos < Pos(len(l.input)) {
|
||||
r := l.input[l.pos]
|
||||
if r < '0' || '9' < r {
|
||||
return lexField
|
||||
}
|
||||
}
|
||||
fallthrough // '.' can start a number.
|
||||
case r == '+' || r == '-' || ('0' <= r && r <= '9'):
|
||||
l.backup()
|
||||
return lexNumber
|
||||
case isAlphaNumeric(r):
|
||||
l.backup()
|
||||
return lexIdentifier
|
||||
case r == '(':
|
||||
l.emit(itemLeftParen)
|
||||
l.parenDepth++
|
||||
case r == ')':
|
||||
l.emit(itemRightParen)
|
||||
l.parenDepth--
|
||||
if l.parenDepth < 0 {
|
||||
return l.errorf("unexpected right paren %#U", r)
|
||||
}
|
||||
case r <= unicode.MaxASCII && unicode.IsPrint(r):
|
||||
l.emit(itemChar)
|
||||
return lexInsideAction
|
||||
default:
|
||||
return l.errorf("unrecognized character in action: %#U", r)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexSpace scans a run of space characters.
|
||||
// We have not consumed the first space, which is known to be present.
|
||||
// Take care if there is a trim-marked right delimiter, which starts with a space.
|
||||
func lexSpace(l *lexer) stateFn {
|
||||
var r rune
|
||||
var numSpaces int
|
||||
for {
|
||||
r = l.peek()
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
l.next()
|
||||
numSpaces++
|
||||
}
|
||||
// Be careful about a trim-marked closing delimiter, which has a minus
|
||||
// after a space. We know there is a space, so check for the '-' that might follow.
|
||||
if strings.HasPrefix(l.input[l.pos-1:], l.trimRightDelim) {
|
||||
l.backup() // Before the space.
|
||||
if numSpaces == 1 {
|
||||
return lexRightDelim // On the delim, so go right to that.
|
||||
}
|
||||
}
|
||||
l.emit(itemSpace)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexIdentifier scans an alphanumeric.
|
||||
func lexIdentifier(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch r := l.next(); {
|
||||
case isAlphaNumeric(r):
|
||||
// absorb.
|
||||
default:
|
||||
l.backup()
|
||||
word := l.input[l.start:l.pos]
|
||||
if !l.atTerminator() {
|
||||
return l.errorf("bad character %#U", r)
|
||||
}
|
||||
switch {
|
||||
case key[word] > itemKeyword:
|
||||
l.emit(key[word])
|
||||
case word[0] == '.':
|
||||
l.emit(itemField)
|
||||
case word == "true", word == "false":
|
||||
l.emit(itemBool)
|
||||
default:
|
||||
l.emit(itemIdentifier)
|
||||
}
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexField scans a field: .Alphanumeric.
|
||||
// The . has been scanned.
|
||||
func lexField(l *lexer) stateFn {
|
||||
return lexFieldOrVariable(l, itemField)
|
||||
}
|
||||
|
||||
// lexVariable scans a Variable: $Alphanumeric.
|
||||
// The $ has been scanned.
|
||||
func lexVariable(l *lexer) stateFn {
|
||||
if l.atTerminator() { // Nothing interesting follows -> "$".
|
||||
l.emit(itemVariable)
|
||||
return lexInsideAction
|
||||
}
|
||||
return lexFieldOrVariable(l, itemVariable)
|
||||
}
|
||||
|
||||
// lexVariable scans a field or variable: [.$]Alphanumeric.
|
||||
// The . or $ has been scanned.
|
||||
func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
|
||||
if l.atTerminator() { // Nothing interesting follows -> "." or "$".
|
||||
if typ == itemVariable {
|
||||
l.emit(itemVariable)
|
||||
} else {
|
||||
l.emit(itemDot)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
var r rune
|
||||
for {
|
||||
r = l.next()
|
||||
if !isAlphaNumeric(r) {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
if !l.atTerminator() {
|
||||
return l.errorf("bad character %#U", r)
|
||||
}
|
||||
l.emit(typ)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// atTerminator reports whether the input is at valid termination character to
|
||||
// appear after an identifier. Breaks .X.Y into two pieces. Also catches cases
|
||||
// like "$x+2" not being acceptable without a space, in case we decide one
|
||||
// day to implement arithmetic.
|
||||
func (l *lexer) atTerminator() bool {
|
||||
r := l.peek()
|
||||
if isSpace(r) || isEndOfLine(r) {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case eof, '.', ',', '|', ':', ')', '(':
|
||||
return true
|
||||
}
|
||||
// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
|
||||
// succeed but should fail) but only in extremely rare cases caused by willfully
|
||||
// bad choice of delimiter.
|
||||
if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// lexChar scans a character constant. The initial quote is already
|
||||
// scanned. Syntax checking is done by the parser.
|
||||
func lexChar(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case '\\':
|
||||
if r := l.next(); r != eof && r != '\n' {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case eof, '\n':
|
||||
return l.errorf("unterminated character constant")
|
||||
case '\'':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemCharConstant)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
||||
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
||||
// and "089" - but when it's wrong the input is invalid and the parser (via
|
||||
// strconv) will notice.
|
||||
func lexNumber(l *lexer) stateFn {
|
||||
if !l.scanNumber() {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
if sign := l.peek(); sign == '+' || sign == '-' {
|
||||
// Complex: 1+2i. No spaces, must end in 'i'.
|
||||
if !l.scanNumber() || l.input[l.pos-1] != 'i' {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
l.emit(itemComplex)
|
||||
} else {
|
||||
l.emit(itemNumber)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
func (l *lexer) scanNumber() bool {
|
||||
// Optional leading sign.
|
||||
l.accept("+-")
|
||||
// Is it hex?
|
||||
digits := "0123456789_"
|
||||
if l.accept("0") {
|
||||
// Note: Leading 0 does not mean octal in floats.
|
||||
if l.accept("xX") {
|
||||
digits = "0123456789abcdefABCDEF_"
|
||||
} else if l.accept("oO") {
|
||||
digits = "01234567_"
|
||||
} else if l.accept("bB") {
|
||||
digits = "01_"
|
||||
}
|
||||
}
|
||||
l.acceptRun(digits)
|
||||
if l.accept(".") {
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
if len(digits) == 10+1 && l.accept("eE") {
|
||||
l.accept("+-")
|
||||
l.acceptRun("0123456789_")
|
||||
}
|
||||
if len(digits) == 16+6+1 && l.accept("pP") {
|
||||
l.accept("+-")
|
||||
l.acceptRun("0123456789_")
|
||||
}
|
||||
// Is it imaginary?
|
||||
l.accept("i")
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if isAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// lexQuote scans a quoted string.
|
||||
func lexQuote(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case '\\':
|
||||
if r := l.next(); r != eof && r != '\n' {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case eof, '\n':
|
||||
return l.errorf("unterminated quoted string")
|
||||
case '"':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemString)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexRawQuote scans a raw quoted string.
|
||||
func lexRawQuote(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case eof:
|
||||
return l.errorf("unterminated raw quoted string")
|
||||
case '`':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemRawString)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// isSpace reports whether r is a space character.
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
// isEndOfLine reports whether r is an end-of-line character.
|
||||
func isEndOfLine(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
556
tpl/internal/go_templates/texttemplate/parse/lex_test.go
Normal file
556
tpl/internal/go_templates/texttemplate/parse/lex_test.go
Normal file
|
@ -0,0 +1,556 @@
|
|||
// Copyright 2011 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 go1.13
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Make the types prettyprint.
|
||||
var itemName = map[itemType]string{
|
||||
itemError: "error",
|
||||
itemBool: "bool",
|
||||
itemChar: "char",
|
||||
itemCharConstant: "charconst",
|
||||
itemComplex: "complex",
|
||||
itemDeclare: ":=",
|
||||
itemEOF: "EOF",
|
||||
itemField: "field",
|
||||
itemIdentifier: "identifier",
|
||||
itemLeftDelim: "left delim",
|
||||
itemLeftParen: "(",
|
||||
itemNumber: "number",
|
||||
itemPipe: "pipe",
|
||||
itemRawString: "raw string",
|
||||
itemRightDelim: "right delim",
|
||||
itemRightParen: ")",
|
||||
itemSpace: "space",
|
||||
itemString: "string",
|
||||
itemVariable: "variable",
|
||||
|
||||
// keywords
|
||||
itemDot: ".",
|
||||
itemBlock: "block",
|
||||
itemDefine: "define",
|
||||
itemElse: "else",
|
||||
itemIf: "if",
|
||||
itemEnd: "end",
|
||||
itemNil: "nil",
|
||||
itemRange: "range",
|
||||
itemTemplate: "template",
|
||||
itemWith: "with",
|
||||
}
|
||||
|
||||
func (i itemType) String() string {
|
||||
s := itemName[i]
|
||||
if s == "" {
|
||||
return fmt.Sprintf("item%d", int(i))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type lexTest struct {
|
||||
name string
|
||||
input string
|
||||
items []item
|
||||
}
|
||||
|
||||
func mkItem(typ itemType, text string) item {
|
||||
return item{
|
||||
typ: typ,
|
||||
val: text,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
tDot = mkItem(itemDot, ".")
|
||||
tBlock = mkItem(itemBlock, "block")
|
||||
tEOF = mkItem(itemEOF, "")
|
||||
tFor = mkItem(itemIdentifier, "for")
|
||||
tLeft = mkItem(itemLeftDelim, "{{")
|
||||
tLpar = mkItem(itemLeftParen, "(")
|
||||
tPipe = mkItem(itemPipe, "|")
|
||||
tQuote = mkItem(itemString, `"abc \n\t\" "`)
|
||||
tRange = mkItem(itemRange, "range")
|
||||
tRight = mkItem(itemRightDelim, "}}")
|
||||
tRpar = mkItem(itemRightParen, ")")
|
||||
tSpace = mkItem(itemSpace, " ")
|
||||
raw = "`" + `abc\n\t\" ` + "`"
|
||||
rawNL = "`now is{{\n}}the time`" // Contains newline inside raw quote.
|
||||
tRawQuote = mkItem(itemRawString, raw)
|
||||
tRawQuoteNL = mkItem(itemRawString, rawNL)
|
||||
)
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"spaces", " \t\n", []item{mkItem(itemText, " \t\n"), tEOF}},
|
||||
{"text", `now is the time`, []item{mkItem(itemText, "now is the time"), tEOF}},
|
||||
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
mkItem(itemText, "-world"),
|
||||
tEOF,
|
||||
}},
|
||||
{"punctuation", "{{,@% }}", []item{
|
||||
tLeft,
|
||||
mkItem(itemChar, ","),
|
||||
mkItem(itemChar, "@"),
|
||||
mkItem(itemChar, "%"),
|
||||
tSpace,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"parens", "{{((3))}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
tLpar,
|
||||
mkItem(itemNumber, "3"),
|
||||
tRpar,
|
||||
tRpar,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
||||
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
||||
{"block", `{{block "foo" .}}`, []item{
|
||||
tLeft, tBlock, tSpace, mkItem(itemString, `"foo"`), tSpace, tDot, tRight, tEOF,
|
||||
}},
|
||||
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
||||
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
||||
{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
|
||||
{"numbers", "{{1 02 0x14 0X14 -7.2i 1e3 1E3 +1.2e-4 4.2i 1+2i 1_2 0x1.e_fp4 0X1.E_FP4}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemNumber, "1"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "02"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "0x14"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "0X14"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "-7.2i"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "1e3"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "1E3"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "+1.2e-4"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "4.2i"),
|
||||
tSpace,
|
||||
mkItem(itemComplex, "1+2i"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "1_2"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "0x1.e_fp4"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "0X1.E_FP4"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
|
||||
tLeft,
|
||||
mkItem(itemCharConstant, `'a'`),
|
||||
tSpace,
|
||||
mkItem(itemCharConstant, `'\n'`),
|
||||
tSpace,
|
||||
mkItem(itemCharConstant, `'\''`),
|
||||
tSpace,
|
||||
mkItem(itemCharConstant, `'\\'`),
|
||||
tSpace,
|
||||
mkItem(itemCharConstant, `'\u00FF'`),
|
||||
tSpace,
|
||||
mkItem(itemCharConstant, `'\xFF'`),
|
||||
tSpace,
|
||||
mkItem(itemCharConstant, `'本'`),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"bools", "{{true false}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemBool, "true"),
|
||||
tSpace,
|
||||
mkItem(itemBool, "false"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dot", "{{.}}", []item{
|
||||
tLeft,
|
||||
tDot,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"nil", "{{nil}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemNil, "nil"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dots", "{{.x . .2 .x.y.z}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemField, ".x"),
|
||||
tSpace,
|
||||
tDot,
|
||||
tSpace,
|
||||
mkItem(itemNumber, ".2"),
|
||||
tSpace,
|
||||
mkItem(itemField, ".x"),
|
||||
mkItem(itemField, ".y"),
|
||||
mkItem(itemField, ".z"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"keywords", "{{range if else end with}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemRange, "range"),
|
||||
tSpace,
|
||||
mkItem(itemIf, "if"),
|
||||
tSpace,
|
||||
mkItem(itemElse, "else"),
|
||||
tSpace,
|
||||
mkItem(itemEnd, "end"),
|
||||
tSpace,
|
||||
mkItem(itemWith, "with"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemVariable, "$c"),
|
||||
tSpace,
|
||||
mkItem(itemDeclare, ":="),
|
||||
tSpace,
|
||||
mkItem(itemIdentifier, "printf"),
|
||||
tSpace,
|
||||
mkItem(itemVariable, "$"),
|
||||
tSpace,
|
||||
mkItem(itemVariable, "$hello"),
|
||||
tSpace,
|
||||
mkItem(itemVariable, "$23"),
|
||||
tSpace,
|
||||
mkItem(itemVariable, "$"),
|
||||
tSpace,
|
||||
mkItem(itemVariable, "$var"),
|
||||
mkItem(itemField, ".Field"),
|
||||
tSpace,
|
||||
mkItem(itemField, ".Method"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variable invocation", "{{$x 23}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemVariable, "$x"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "23"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
|
||||
mkItem(itemText, "intro "),
|
||||
tLeft,
|
||||
mkItem(itemIdentifier, "echo"),
|
||||
tSpace,
|
||||
mkItem(itemIdentifier, "hi"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "1.2"),
|
||||
tSpace,
|
||||
tPipe,
|
||||
mkItem(itemIdentifier, "noargs"),
|
||||
tPipe,
|
||||
mkItem(itemIdentifier, "args"),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "1"),
|
||||
tSpace,
|
||||
mkItem(itemString, `"hi"`),
|
||||
tRight,
|
||||
mkItem(itemText, " outro"),
|
||||
tEOF,
|
||||
}},
|
||||
{"declaration", "{{$v := 3}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemVariable, "$v"),
|
||||
tSpace,
|
||||
mkItem(itemDeclare, ":="),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "3"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"2 declarations", "{{$v , $w := 3}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemVariable, "$v"),
|
||||
tSpace,
|
||||
mkItem(itemChar, ","),
|
||||
tSpace,
|
||||
mkItem(itemVariable, "$w"),
|
||||
tSpace,
|
||||
mkItem(itemDeclare, ":="),
|
||||
tSpace,
|
||||
mkItem(itemNumber, "3"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"field of parenthesized expression", "{{(.X).Y}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
mkItem(itemField, ".X"),
|
||||
tRpar,
|
||||
mkItem(itemField, ".Y"),
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
tLeft,
|
||||
mkItem(itemNumber, "3"),
|
||||
tRight,
|
||||
mkItem(itemText, "-world"),
|
||||
tEOF,
|
||||
}},
|
||||
{"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
mkItem(itemText, "-world"),
|
||||
tEOF,
|
||||
}},
|
||||
// errors
|
||||
{"badchar", "#{{\x01}}", []item{
|
||||
mkItem(itemText, "#"),
|
||||
tLeft,
|
||||
mkItem(itemError, "unrecognized character in action: U+0001"),
|
||||
}},
|
||||
{"unclosed action", "{{\n}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemError, "unclosed action"),
|
||||
}},
|
||||
{"EOF in action", "{{range", []item{
|
||||
tLeft,
|
||||
tRange,
|
||||
mkItem(itemError, "unclosed action"),
|
||||
}},
|
||||
{"unclosed quote", "{{\"\n\"}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemError, "unterminated quoted string"),
|
||||
}},
|
||||
{"unclosed raw quote", "{{`xx}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemError, "unterminated raw quoted string"),
|
||||
}},
|
||||
{"unclosed char constant", "{{'\n}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemError, "unterminated character constant"),
|
||||
}},
|
||||
{"bad number", "{{3k}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemError, `bad number syntax: "3k"`),
|
||||
}},
|
||||
{"unclosed paren", "{{(3}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
mkItem(itemNumber, "3"),
|
||||
mkItem(itemError, `unclosed left paren`),
|
||||
}},
|
||||
{"extra right paren", "{{3)}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemNumber, "3"),
|
||||
tRpar,
|
||||
mkItem(itemError, `unexpected right paren U+0029 ')'`),
|
||||
}},
|
||||
|
||||
// Fixed bugs
|
||||
// Many elements in an action blew the lookahead until
|
||||
// we made lexInsideAction not loop.
|
||||
{"long pipeline deadlock", "{{|||||}}", []item{
|
||||
tLeft,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"text with bad comment", "hello-{{/*/}}-world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
mkItem(itemError, `unclosed comment`),
|
||||
}},
|
||||
{"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
mkItem(itemError, `comment ends before closing delimiter`),
|
||||
}},
|
||||
// This one is an error that we can't catch because it breaks templates with
|
||||
// minimized JavaScript. Should have fixed it before Go 1.1.
|
||||
{"unmatched right delimiter", "hello-{.}}-world", []item{
|
||||
mkItem(itemText, "hello-{.}}-world"),
|
||||
tEOF,
|
||||
}},
|
||||
}
|
||||
|
||||
// collect gathers the emitted items into a slice.
|
||||
func collect(t *lexTest, left, right string) (items []item) {
|
||||
l := lex(t.name, t.input, left, right)
|
||||
for {
|
||||
item := l.nextItem()
|
||||
items = append(items, item)
|
||||
if item.typ == itemEOF || item.typ == itemError {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func equal(i1, i2 []item, checkPos bool) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
for k := range i1 {
|
||||
if i1[k].typ != i2[k].typ {
|
||||
return false
|
||||
}
|
||||
if i1[k].val != i2[k].val {
|
||||
return false
|
||||
}
|
||||
if checkPos && i1[k].pos != i2[k].pos {
|
||||
return false
|
||||
}
|
||||
if checkPos && i1[k].line != i2[k].line {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
for _, test := range lexTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some easy cases from above, but with delimiters $$ and @@
|
||||
var lexDelimTests = []lexTest{
|
||||
{"punctuation", "$$,@%{{}}@@", []item{
|
||||
tLeftDelim,
|
||||
mkItem(itemChar, ","),
|
||||
mkItem(itemChar, "@"),
|
||||
mkItem(itemChar, "%"),
|
||||
mkItem(itemChar, "{"),
|
||||
mkItem(itemChar, "{"),
|
||||
mkItem(itemChar, "}"),
|
||||
mkItem(itemChar, "}"),
|
||||
tRightDelim,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `$$@@`, []item{tLeftDelim, tRightDelim, tEOF}},
|
||||
{"for", `$$for@@`, []item{tLeftDelim, tFor, tRightDelim, tEOF}},
|
||||
{"quote", `$$"abc \n\t\" "@@`, []item{tLeftDelim, tQuote, tRightDelim, tEOF}},
|
||||
{"raw quote", "$$" + raw + "@@", []item{tLeftDelim, tRawQuote, tRightDelim, tEOF}},
|
||||
}
|
||||
|
||||
var (
|
||||
tLeftDelim = mkItem(itemLeftDelim, "$$")
|
||||
tRightDelim = mkItem(itemRightDelim, "@@")
|
||||
)
|
||||
|
||||
func TestDelims(t *testing.T) {
|
||||
for _, test := range lexDelimTests {
|
||||
items := collect(&test, "$$", "@@")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lexPosTests = []lexTest{
|
||||
{"empty", "", []item{{itemEOF, 0, "", 1}}},
|
||||
{"punctuation", "{{,@%#}}", []item{
|
||||
{itemLeftDelim, 0, "{{", 1},
|
||||
{itemChar, 2, ",", 1},
|
||||
{itemChar, 3, "@", 1},
|
||||
{itemChar, 4, "%", 1},
|
||||
{itemChar, 5, "#", 1},
|
||||
{itemRightDelim, 6, "}}", 1},
|
||||
{itemEOF, 8, "", 1},
|
||||
}},
|
||||
{"sample", "0123{{hello}}xyz", []item{
|
||||
{itemText, 0, "0123", 1},
|
||||
{itemLeftDelim, 4, "{{", 1},
|
||||
{itemIdentifier, 6, "hello", 1},
|
||||
{itemRightDelim, 11, "}}", 1},
|
||||
{itemText, 13, "xyz", 1},
|
||||
{itemEOF, 16, "", 1},
|
||||
}},
|
||||
{"trimafter", "{{x -}}\n{{y}}", []item{
|
||||
{itemLeftDelim, 0, "{{", 1},
|
||||
{itemIdentifier, 2, "x", 1},
|
||||
{itemRightDelim, 5, "}}", 1},
|
||||
{itemLeftDelim, 8, "{{", 2},
|
||||
{itemIdentifier, 10, "y", 2},
|
||||
{itemRightDelim, 11, "}}", 2},
|
||||
{itemEOF, 13, "", 2},
|
||||
}},
|
||||
{"trimbefore", "{{x}}\n{{- y}}", []item{
|
||||
{itemLeftDelim, 0, "{{", 1},
|
||||
{itemIdentifier, 2, "x", 1},
|
||||
{itemRightDelim, 3, "}}", 1},
|
||||
{itemLeftDelim, 6, "{{", 2},
|
||||
{itemIdentifier, 10, "y", 2},
|
||||
{itemRightDelim, 11, "}}", 2},
|
||||
{itemEOF, 13, "", 2},
|
||||
}},
|
||||
}
|
||||
|
||||
// The other tests don't check position, to make the test cases easier to construct.
|
||||
// This one does.
|
||||
func TestPos(t *testing.T) {
|
||||
for _, test := range lexPosTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, true) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
if len(items) == len(test.items) {
|
||||
// Detailed print; avoid item.String() to expose the position value.
|
||||
for i := range items {
|
||||
if !equal(items[i:i+1], test.items[i:i+1], true) {
|
||||
i1 := items[i]
|
||||
i2 := test.items[i]
|
||||
t.Errorf("\t#%d: got {%v %d %q %d} expected {%v %d %q %d}",
|
||||
i, i1.typ, i1.pos, i1.val, i1.line, i2.typ, i2.pos, i2.val, i2.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that an error shuts down the lexing goroutine.
|
||||
func TestShutdown(t *testing.T) {
|
||||
// We need to duplicate template.Parse here to hold on to the lexer.
|
||||
const text = "erroneous{{define}}{{else}}1234"
|
||||
lexer := lex("foo", text, "{{", "}}")
|
||||
_, err := New("root").parseLexer(lexer)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
// The error should have drained the input. Therefore, the lexer should be shut down.
|
||||
token, ok := <-lexer.items
|
||||
if ok {
|
||||
t.Fatalf("input was not drained; got %v", token)
|
||||
}
|
||||
}
|
||||
|
||||
// parseLexer is a local version of parse that lets us pass in the lexer instead of building it.
|
||||
// We expect an error, so the tree set and funcs list are explicitly nil.
|
||||
func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {
|
||||
defer t.recover(&err)
|
||||
t.ParseName = t.Name
|
||||
t.startParse(nil, lex, map[string]*Tree{})
|
||||
t.parse()
|
||||
t.add()
|
||||
t.stopParse()
|
||||
return t, nil
|
||||
}
|
841
tpl/internal/go_templates/texttemplate/parse/node.go
Normal file
841
tpl/internal/go_templates/texttemplate/parse/node.go
Normal file
|
@ -0,0 +1,841 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// Parse nodes.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var textFormat = "%s" // Changed to "%q" in tests for better error messages.
|
||||
|
||||
// A Node is an element in the parse tree. The interface is trivial.
|
||||
// The interface contains an unexported method so that only
|
||||
// types local to this package can satisfy it.
|
||||
type Node interface {
|
||||
Type() NodeType
|
||||
String() string
|
||||
// Copy does a deep copy of the Node and all its components.
|
||||
// To avoid type assertions, some XxxNodes also have specialized
|
||||
// CopyXxx methods that return *XxxNode.
|
||||
Copy() Node
|
||||
Position() Pos // byte position of start of node in full original input string
|
||||
// tree returns the containing *Tree.
|
||||
// It is unexported so all implementations of Node are in this package.
|
||||
tree() *Tree
|
||||
}
|
||||
|
||||
// NodeType identifies the type of a parse tree node.
|
||||
type NodeType int
|
||||
|
||||
// Pos represents a byte position in the original input text from which
|
||||
// this template was parsed.
|
||||
type Pos int
|
||||
|
||||
func (p Pos) Position() Pos {
|
||||
return p
|
||||
}
|
||||
|
||||
// Type returns itself and provides an easy default implementation
|
||||
// for embedding in a Node. Embedded in all non-trivial Nodes.
|
||||
func (t NodeType) Type() NodeType {
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
NodeText NodeType = iota // Plain text.
|
||||
NodeAction // A non-control action such as a field evaluation.
|
||||
NodeBool // A boolean constant.
|
||||
NodeChain // A sequence of field accesses.
|
||||
NodeCommand // An element of a pipeline.
|
||||
NodeDot // The cursor, dot.
|
||||
nodeElse // An else action. Not added to tree.
|
||||
nodeEnd // An end action. Not added to tree.
|
||||
NodeField // A field or method name.
|
||||
NodeIdentifier // An identifier; always a function name.
|
||||
NodeIf // An if action.
|
||||
NodeList // A list of Nodes.
|
||||
NodeNil // An untyped nil constant.
|
||||
NodeNumber // A numerical constant.
|
||||
NodePipe // A pipeline of commands.
|
||||
NodeRange // A range action.
|
||||
NodeString // A string constant.
|
||||
NodeTemplate // A template invocation action.
|
||||
NodeVariable // A $ variable.
|
||||
NodeWith // A with action.
|
||||
)
|
||||
|
||||
// Nodes.
|
||||
|
||||
// ListNode holds a sequence of nodes.
|
||||
type ListNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Nodes []Node // The element nodes in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newList(pos Pos) *ListNode {
|
||||
return &ListNode{tr: t, NodeType: NodeList, Pos: pos}
|
||||
}
|
||||
|
||||
func (l *ListNode) append(n Node) {
|
||||
l.Nodes = append(l.Nodes, n)
|
||||
}
|
||||
|
||||
func (l *ListNode) tree() *Tree {
|
||||
return l.tr
|
||||
}
|
||||
|
||||
func (l *ListNode) String() string {
|
||||
b := new(bytes.Buffer)
|
||||
for _, n := range l.Nodes {
|
||||
fmt.Fprint(b, n)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (l *ListNode) CopyList() *ListNode {
|
||||
if l == nil {
|
||||
return l
|
||||
}
|
||||
n := l.tr.newList(l.Pos)
|
||||
for _, elem := range l.Nodes {
|
||||
n.append(elem.Copy())
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (l *ListNode) Copy() Node {
|
||||
return l.CopyList()
|
||||
}
|
||||
|
||||
// TextNode holds plain text.
|
||||
type TextNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Text []byte // The text; may span newlines.
|
||||
}
|
||||
|
||||
func (t *Tree) newText(pos Pos, text string) *TextNode {
|
||||
return &TextNode{tr: t, NodeType: NodeText, Pos: pos, Text: []byte(text)}
|
||||
}
|
||||
|
||||
func (t *TextNode) String() string {
|
||||
return fmt.Sprintf(textFormat, t.Text)
|
||||
}
|
||||
|
||||
func (t *TextNode) tree() *Tree {
|
||||
return t.tr
|
||||
}
|
||||
|
||||
func (t *TextNode) Copy() Node {
|
||||
return &TextNode{tr: t.tr, NodeType: NodeText, Pos: t.Pos, Text: append([]byte{}, t.Text...)}
|
||||
}
|
||||
|
||||
// PipeNode holds a pipeline with optional declaration
|
||||
type PipeNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input. Deprecated: Kept for compatibility.
|
||||
IsAssign bool // The variables are being assigned, not declared.
|
||||
Decl []*VariableNode // Variables in lexical order.
|
||||
Cmds []*CommandNode // The commands in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newPipeline(pos Pos, line int, vars []*VariableNode) *PipeNode {
|
||||
return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Decl: vars}
|
||||
}
|
||||
|
||||
func (p *PipeNode) append(command *CommandNode) {
|
||||
p.Cmds = append(p.Cmds, command)
|
||||
}
|
||||
|
||||
func (p *PipeNode) String() string {
|
||||
s := ""
|
||||
if len(p.Decl) > 0 {
|
||||
for i, v := range p.Decl {
|
||||
if i > 0 {
|
||||
s += ", "
|
||||
}
|
||||
s += v.String()
|
||||
}
|
||||
s += " := "
|
||||
}
|
||||
for i, c := range p.Cmds {
|
||||
if i > 0 {
|
||||
s += " | "
|
||||
}
|
||||
s += c.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *PipeNode) tree() *Tree {
|
||||
return p.tr
|
||||
}
|
||||
|
||||
func (p *PipeNode) CopyPipe() *PipeNode {
|
||||
if p == nil {
|
||||
return p
|
||||
}
|
||||
var vars []*VariableNode
|
||||
for _, d := range p.Decl {
|
||||
vars = append(vars, d.Copy().(*VariableNode))
|
||||
}
|
||||
n := p.tr.newPipeline(p.Pos, p.Line, vars)
|
||||
n.IsAssign = p.IsAssign
|
||||
for _, c := range p.Cmds {
|
||||
n.append(c.Copy().(*CommandNode))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (p *PipeNode) Copy() Node {
|
||||
return p.CopyPipe()
|
||||
}
|
||||
|
||||
// ActionNode holds an action (something bounded by delimiters).
|
||||
// Control actions have their own nodes; ActionNode represents simple
|
||||
// ones such as field evaluations and parenthesized pipelines.
|
||||
type ActionNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input. Deprecated: Kept for compatibility.
|
||||
Pipe *PipeNode // The pipeline in the action.
|
||||
}
|
||||
|
||||
func (t *Tree) newAction(pos Pos, line int, pipe *PipeNode) *ActionNode {
|
||||
return &ActionNode{tr: t, NodeType: NodeAction, Pos: pos, Line: line, Pipe: pipe}
|
||||
}
|
||||
|
||||
func (a *ActionNode) String() string {
|
||||
return fmt.Sprintf("{{%s}}", a.Pipe)
|
||||
|
||||
}
|
||||
|
||||
func (a *ActionNode) tree() *Tree {
|
||||
return a.tr
|
||||
}
|
||||
|
||||
func (a *ActionNode) Copy() Node {
|
||||
return a.tr.newAction(a.Pos, a.Line, a.Pipe.CopyPipe())
|
||||
|
||||
}
|
||||
|
||||
// CommandNode holds a command (a pipeline inside an evaluating action).
|
||||
type CommandNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Args []Node // Arguments in lexical order: Identifier, field, or constant.
|
||||
}
|
||||
|
||||
func (t *Tree) newCommand(pos Pos) *CommandNode {
|
||||
return &CommandNode{tr: t, NodeType: NodeCommand, Pos: pos}
|
||||
}
|
||||
|
||||
func (c *CommandNode) append(arg Node) {
|
||||
c.Args = append(c.Args, arg)
|
||||
}
|
||||
|
||||
func (c *CommandNode) String() string {
|
||||
s := ""
|
||||
for i, arg := range c.Args {
|
||||
if i > 0 {
|
||||
s += " "
|
||||
}
|
||||
if arg, ok := arg.(*PipeNode); ok {
|
||||
s += "(" + arg.String() + ")"
|
||||
continue
|
||||
}
|
||||
s += arg.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *CommandNode) tree() *Tree {
|
||||
return c.tr
|
||||
}
|
||||
|
||||
func (c *CommandNode) Copy() Node {
|
||||
if c == nil {
|
||||
return c
|
||||
}
|
||||
n := c.tr.newCommand(c.Pos)
|
||||
for _, c := range c.Args {
|
||||
n.append(c.Copy())
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// IdentifierNode holds an identifier.
|
||||
type IdentifierNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Ident string // The identifier's name.
|
||||
}
|
||||
|
||||
// NewIdentifier returns a new IdentifierNode with the given identifier name.
|
||||
func NewIdentifier(ident string) *IdentifierNode {
|
||||
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
|
||||
}
|
||||
|
||||
// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
|
||||
// Chained for convenience.
|
||||
// TODO: fix one day?
|
||||
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
||||
i.Pos = pos
|
||||
return i
|
||||
}
|
||||
|
||||
// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
|
||||
// Chained for convenience.
|
||||
// TODO: fix one day?
|
||||
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
|
||||
i.tr = t
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *IdentifierNode) String() string {
|
||||
return i.Ident
|
||||
}
|
||||
|
||||
func (i *IdentifierNode) tree() *Tree {
|
||||
return i.tr
|
||||
}
|
||||
|
||||
func (i *IdentifierNode) Copy() Node {
|
||||
return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos)
|
||||
}
|
||||
|
||||
// AssignNode holds a list of variable names, possibly with chained field
|
||||
// accesses. The dollar sign is part of the (first) name.
|
||||
type VariableNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Ident []string // Variable name and fields in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
|
||||
return &VariableNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
|
||||
}
|
||||
|
||||
func (v *VariableNode) String() string {
|
||||
s := ""
|
||||
for i, id := range v.Ident {
|
||||
if i > 0 {
|
||||
s += "."
|
||||
}
|
||||
s += id
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (v *VariableNode) tree() *Tree {
|
||||
return v.tr
|
||||
}
|
||||
|
||||
func (v *VariableNode) Copy() Node {
|
||||
return &VariableNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
|
||||
}
|
||||
|
||||
// DotNode holds the special identifier '.'.
|
||||
type DotNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
}
|
||||
|
||||
func (t *Tree) newDot(pos Pos) *DotNode {
|
||||
return &DotNode{tr: t, NodeType: NodeDot, Pos: pos}
|
||||
}
|
||||
|
||||
func (d *DotNode) Type() NodeType {
|
||||
// Override method on embedded NodeType for API compatibility.
|
||||
// TODO: Not really a problem; could change API without effect but
|
||||
// api tool complains.
|
||||
return NodeDot
|
||||
}
|
||||
|
||||
func (d *DotNode) String() string {
|
||||
return "."
|
||||
}
|
||||
|
||||
func (d *DotNode) tree() *Tree {
|
||||
return d.tr
|
||||
}
|
||||
|
||||
func (d *DotNode) Copy() Node {
|
||||
return d.tr.newDot(d.Pos)
|
||||
}
|
||||
|
||||
// NilNode holds the special identifier 'nil' representing an untyped nil constant.
|
||||
type NilNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
}
|
||||
|
||||
func (t *Tree) newNil(pos Pos) *NilNode {
|
||||
return &NilNode{tr: t, NodeType: NodeNil, Pos: pos}
|
||||
}
|
||||
|
||||
func (n *NilNode) Type() NodeType {
|
||||
// Override method on embedded NodeType for API compatibility.
|
||||
// TODO: Not really a problem; could change API without effect but
|
||||
// api tool complains.
|
||||
return NodeNil
|
||||
}
|
||||
|
||||
func (n *NilNode) String() string {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
func (n *NilNode) tree() *Tree {
|
||||
return n.tr
|
||||
}
|
||||
|
||||
func (n *NilNode) Copy() Node {
|
||||
return n.tr.newNil(n.Pos)
|
||||
}
|
||||
|
||||
// FieldNode holds a field (identifier starting with '.').
|
||||
// The names may be chained ('.x.y').
|
||||
// The period is dropped from each ident.
|
||||
type FieldNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Ident []string // The identifiers in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newField(pos Pos, ident string) *FieldNode {
|
||||
return &FieldNode{tr: t, NodeType: NodeField, Pos: pos, Ident: strings.Split(ident[1:], ".")} // [1:] to drop leading period
|
||||
}
|
||||
|
||||
func (f *FieldNode) String() string {
|
||||
s := ""
|
||||
for _, id := range f.Ident {
|
||||
s += "." + id
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (f *FieldNode) tree() *Tree {
|
||||
return f.tr
|
||||
}
|
||||
|
||||
func (f *FieldNode) Copy() Node {
|
||||
return &FieldNode{tr: f.tr, NodeType: NodeField, Pos: f.Pos, Ident: append([]string{}, f.Ident...)}
|
||||
}
|
||||
|
||||
// ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
|
||||
// The names may be chained ('.x.y').
|
||||
// The periods are dropped from each ident.
|
||||
type ChainNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Node Node
|
||||
Field []string // The identifiers in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newChain(pos Pos, node Node) *ChainNode {
|
||||
return &ChainNode{tr: t, NodeType: NodeChain, Pos: pos, Node: node}
|
||||
}
|
||||
|
||||
// Add adds the named field (which should start with a period) to the end of the chain.
|
||||
func (c *ChainNode) Add(field string) {
|
||||
if len(field) == 0 || field[0] != '.' {
|
||||
panic("no dot in field")
|
||||
}
|
||||
field = field[1:] // Remove leading dot.
|
||||
if field == "" {
|
||||
panic("empty field")
|
||||
}
|
||||
c.Field = append(c.Field, field)
|
||||
}
|
||||
|
||||
func (c *ChainNode) String() string {
|
||||
s := c.Node.String()
|
||||
if _, ok := c.Node.(*PipeNode); ok {
|
||||
s = "(" + s + ")"
|
||||
}
|
||||
for _, field := range c.Field {
|
||||
s += "." + field
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *ChainNode) tree() *Tree {
|
||||
return c.tr
|
||||
}
|
||||
|
||||
func (c *ChainNode) Copy() Node {
|
||||
return &ChainNode{tr: c.tr, NodeType: NodeChain, Pos: c.Pos, Node: c.Node, Field: append([]string{}, c.Field...)}
|
||||
}
|
||||
|
||||
// BoolNode holds a boolean constant.
|
||||
type BoolNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
True bool // The value of the boolean constant.
|
||||
}
|
||||
|
||||
func (t *Tree) newBool(pos Pos, true bool) *BoolNode {
|
||||
return &BoolNode{tr: t, NodeType: NodeBool, Pos: pos, True: true}
|
||||
}
|
||||
|
||||
func (b *BoolNode) String() string {
|
||||
if b.True {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (b *BoolNode) tree() *Tree {
|
||||
return b.tr
|
||||
}
|
||||
|
||||
func (b *BoolNode) Copy() Node {
|
||||
return b.tr.newBool(b.Pos, b.True)
|
||||
}
|
||||
|
||||
// NumberNode holds a number: signed or unsigned integer, float, or complex.
|
||||
// The value is parsed and stored under all the types that can represent the value.
|
||||
// This simulates in a small amount of code the behavior of Go's ideal constants.
|
||||
type NumberNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
IsInt bool // Number has an integral value.
|
||||
IsUint bool // Number has an unsigned integral value.
|
||||
IsFloat bool // Number has a floating-point value.
|
||||
IsComplex bool // Number is complex.
|
||||
Int64 int64 // The signed integer value.
|
||||
Uint64 uint64 // The unsigned integer value.
|
||||
Float64 float64 // The floating-point value.
|
||||
Complex128 complex128 // The complex value.
|
||||
Text string // The original textual representation from the input.
|
||||
}
|
||||
|
||||
func (t *Tree) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
|
||||
n := &NumberNode{tr: t, NodeType: NodeNumber, Pos: pos, Text: text}
|
||||
switch typ {
|
||||
case itemCharConstant:
|
||||
rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tail != "'" {
|
||||
return nil, fmt.Errorf("malformed character constant: %s", text)
|
||||
}
|
||||
n.Int64 = int64(rune)
|
||||
n.IsInt = true
|
||||
n.Uint64 = uint64(rune)
|
||||
n.IsUint = true
|
||||
n.Float64 = float64(rune) // odd but those are the rules.
|
||||
n.IsFloat = true
|
||||
return n, nil
|
||||
case itemComplex:
|
||||
// fmt.Sscan can parse the pair, so let it do the work.
|
||||
if _, err := fmt.Sscan(text, &n.Complex128); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.IsComplex = true
|
||||
n.simplifyComplex()
|
||||
return n, nil
|
||||
}
|
||||
// Imaginary constants can only be complex unless they are zero.
|
||||
if len(text) > 0 && text[len(text)-1] == 'i' {
|
||||
f, err := strconv.ParseFloat(text[:len(text)-1], 64)
|
||||
if err == nil {
|
||||
n.IsComplex = true
|
||||
n.Complex128 = complex(0, f)
|
||||
n.simplifyComplex()
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
// Do integer test first so we get 0x123 etc.
|
||||
u, err := strconv.ParseUint(text, 0, 64) // will fail for -0; fixed below.
|
||||
if err == nil {
|
||||
n.IsUint = true
|
||||
n.Uint64 = u
|
||||
}
|
||||
i, err := strconv.ParseInt(text, 0, 64)
|
||||
if err == nil {
|
||||
n.IsInt = true
|
||||
n.Int64 = i
|
||||
if i == 0 {
|
||||
n.IsUint = true // in case of -0.
|
||||
n.Uint64 = u
|
||||
}
|
||||
}
|
||||
// If an integer extraction succeeded, promote the float.
|
||||
if n.IsInt {
|
||||
n.IsFloat = true
|
||||
n.Float64 = float64(n.Int64)
|
||||
} else if n.IsUint {
|
||||
n.IsFloat = true
|
||||
n.Float64 = float64(n.Uint64)
|
||||
} else {
|
||||
f, err := strconv.ParseFloat(text, 64)
|
||||
if err == nil {
|
||||
// If we parsed it as a float but it looks like an integer,
|
||||
// it's a huge number too large to fit in an int. Reject it.
|
||||
if !strings.ContainsAny(text, ".eEpP") {
|
||||
return nil, fmt.Errorf("integer overflow: %q", text)
|
||||
}
|
||||
n.IsFloat = true
|
||||
n.Float64 = f
|
||||
// If a floating-point extraction succeeded, extract the int if needed.
|
||||
if !n.IsInt && float64(int64(f)) == f {
|
||||
n.IsInt = true
|
||||
n.Int64 = int64(f)
|
||||
}
|
||||
if !n.IsUint && float64(uint64(f)) == f {
|
||||
n.IsUint = true
|
||||
n.Uint64 = uint64(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !n.IsInt && !n.IsUint && !n.IsFloat {
|
||||
return nil, fmt.Errorf("illegal number syntax: %q", text)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// simplifyComplex pulls out any other types that are represented by the complex number.
|
||||
// These all require that the imaginary part be zero.
|
||||
func (n *NumberNode) simplifyComplex() {
|
||||
n.IsFloat = imag(n.Complex128) == 0
|
||||
if n.IsFloat {
|
||||
n.Float64 = real(n.Complex128)
|
||||
n.IsInt = float64(int64(n.Float64)) == n.Float64
|
||||
if n.IsInt {
|
||||
n.Int64 = int64(n.Float64)
|
||||
}
|
||||
n.IsUint = float64(uint64(n.Float64)) == n.Float64
|
||||
if n.IsUint {
|
||||
n.Uint64 = uint64(n.Float64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NumberNode) String() string {
|
||||
return n.Text
|
||||
}
|
||||
|
||||
func (n *NumberNode) tree() *Tree {
|
||||
return n.tr
|
||||
}
|
||||
|
||||
func (n *NumberNode) Copy() Node {
|
||||
nn := new(NumberNode)
|
||||
*nn = *n // Easy, fast, correct.
|
||||
return nn
|
||||
}
|
||||
|
||||
// StringNode holds a string constant. The value has been "unquoted".
|
||||
type StringNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Quoted string // The original text of the string, with quotes.
|
||||
Text string // The string, after quote processing.
|
||||
}
|
||||
|
||||
func (t *Tree) newString(pos Pos, orig, text string) *StringNode {
|
||||
return &StringNode{tr: t, NodeType: NodeString, Pos: pos, Quoted: orig, Text: text}
|
||||
}
|
||||
|
||||
func (s *StringNode) String() string {
|
||||
return s.Quoted
|
||||
}
|
||||
|
||||
func (s *StringNode) tree() *Tree {
|
||||
return s.tr
|
||||
}
|
||||
|
||||
func (s *StringNode) Copy() Node {
|
||||
return s.tr.newString(s.Pos, s.Quoted, s.Text)
|
||||
}
|
||||
|
||||
// endNode represents an {{end}} action.
|
||||
// It does not appear in the final parse tree.
|
||||
type endNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
}
|
||||
|
||||
func (t *Tree) newEnd(pos Pos) *endNode {
|
||||
return &endNode{tr: t, NodeType: nodeEnd, Pos: pos}
|
||||
}
|
||||
|
||||
func (e *endNode) String() string {
|
||||
return "{{end}}"
|
||||
}
|
||||
|
||||
func (e *endNode) tree() *Tree {
|
||||
return e.tr
|
||||
}
|
||||
|
||||
func (e *endNode) Copy() Node {
|
||||
return e.tr.newEnd(e.Pos)
|
||||
}
|
||||
|
||||
// elseNode represents an {{else}} action. Does not appear in the final tree.
|
||||
type elseNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input. Deprecated: Kept for compatibility.
|
||||
}
|
||||
|
||||
func (t *Tree) newElse(pos Pos, line int) *elseNode {
|
||||
return &elseNode{tr: t, NodeType: nodeElse, Pos: pos, Line: line}
|
||||
}
|
||||
|
||||
func (e *elseNode) Type() NodeType {
|
||||
return nodeElse
|
||||
}
|
||||
|
||||
func (e *elseNode) String() string {
|
||||
return "{{else}}"
|
||||
}
|
||||
|
||||
func (e *elseNode) tree() *Tree {
|
||||
return e.tr
|
||||
}
|
||||
|
||||
func (e *elseNode) Copy() Node {
|
||||
return e.tr.newElse(e.Pos, e.Line)
|
||||
}
|
||||
|
||||
// BranchNode is the common representation of if, range, and with.
|
||||
type BranchNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input. Deprecated: Kept for compatibility.
|
||||
Pipe *PipeNode // The pipeline to be evaluated.
|
||||
List *ListNode // What to execute if the value is non-empty.
|
||||
ElseList *ListNode // What to execute if the value is empty (nil if absent).
|
||||
}
|
||||
|
||||
func (b *BranchNode) String() string {
|
||||
name := ""
|
||||
switch b.NodeType {
|
||||
case NodeIf:
|
||||
name = "if"
|
||||
case NodeRange:
|
||||
name = "range"
|
||||
case NodeWith:
|
||||
name = "with"
|
||||
default:
|
||||
panic("unknown branch type")
|
||||
}
|
||||
if b.ElseList != nil {
|
||||
return fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}", name, b.Pipe, b.List, b.ElseList)
|
||||
}
|
||||
return fmt.Sprintf("{{%s %s}}%s{{end}}", name, b.Pipe, b.List)
|
||||
}
|
||||
|
||||
func (b *BranchNode) tree() *Tree {
|
||||
return b.tr
|
||||
}
|
||||
|
||||
func (b *BranchNode) Copy() Node {
|
||||
switch b.NodeType {
|
||||
case NodeIf:
|
||||
return b.tr.newIf(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
|
||||
case NodeRange:
|
||||
return b.tr.newRange(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
|
||||
case NodeWith:
|
||||
return b.tr.newWith(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
|
||||
default:
|
||||
panic("unknown branch type")
|
||||
}
|
||||
}
|
||||
|
||||
// IfNode represents an {{if}} action and its commands.
|
||||
type IfNode struct {
|
||||
BranchNode
|
||||
}
|
||||
|
||||
func (t *Tree) newIf(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *IfNode {
|
||||
return &IfNode{BranchNode{tr: t, NodeType: NodeIf, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
|
||||
}
|
||||
|
||||
func (i *IfNode) Copy() Node {
|
||||
return i.tr.newIf(i.Pos, i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList())
|
||||
}
|
||||
|
||||
// RangeNode represents a {{range}} action and its commands.
|
||||
type RangeNode struct {
|
||||
BranchNode
|
||||
}
|
||||
|
||||
func (t *Tree) newRange(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *RangeNode {
|
||||
return &RangeNode{BranchNode{tr: t, NodeType: NodeRange, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
|
||||
}
|
||||
|
||||
func (r *RangeNode) Copy() Node {
|
||||
return r.tr.newRange(r.Pos, r.Line, r.Pipe.CopyPipe(), r.List.CopyList(), r.ElseList.CopyList())
|
||||
}
|
||||
|
||||
// WithNode represents a {{with}} action and its commands.
|
||||
type WithNode struct {
|
||||
BranchNode
|
||||
}
|
||||
|
||||
func (t *Tree) newWith(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *WithNode {
|
||||
return &WithNode{BranchNode{tr: t, NodeType: NodeWith, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
|
||||
}
|
||||
|
||||
func (w *WithNode) Copy() Node {
|
||||
return w.tr.newWith(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
|
||||
}
|
||||
|
||||
// TemplateNode represents a {{template}} action.
|
||||
type TemplateNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input. Deprecated: Kept for compatibility.
|
||||
Name string // The name of the template (unquoted).
|
||||
Pipe *PipeNode // The command to evaluate as dot for the template.
|
||||
}
|
||||
|
||||
func (t *Tree) newTemplate(pos Pos, line int, name string, pipe *PipeNode) *TemplateNode {
|
||||
return &TemplateNode{tr: t, NodeType: NodeTemplate, Pos: pos, Line: line, Name: name, Pipe: pipe}
|
||||
}
|
||||
|
||||
func (t *TemplateNode) String() string {
|
||||
if t.Pipe == nil {
|
||||
return fmt.Sprintf("{{template %q}}", t.Name)
|
||||
}
|
||||
return fmt.Sprintf("{{template %q %s}}", t.Name, t.Pipe)
|
||||
}
|
||||
|
||||
func (t *TemplateNode) tree() *Tree {
|
||||
return t.tr
|
||||
}
|
||||
|
||||
func (t *TemplateNode) Copy() Node {
|
||||
return t.tr.newTemplate(t.Pos, t.Line, t.Name, t.Pipe.CopyPipe())
|
||||
}
|
736
tpl/internal/go_templates/texttemplate/parse/parse.go
Normal file
736
tpl/internal/go_templates/texttemplate/parse/parse.go
Normal file
|
@ -0,0 +1,736 @@
|
|||
// Copyright 2011 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 parse builds parse trees for templates as defined by text/template
|
||||
// and html/template. Clients should use those packages to construct templates
|
||||
// rather than this one, which provides shared internal data structures not
|
||||
// intended for general use.
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Tree is the representation of a single parsed template.
|
||||
type Tree struct {
|
||||
Name string // name of the template represented by the tree.
|
||||
ParseName string // name of the top-level template during parsing, for error messages.
|
||||
Root *ListNode // top-level root of the tree.
|
||||
text string // text parsed to create the template (or its parent)
|
||||
// Parsing only; cleared after parse.
|
||||
funcs []map[string]interface{}
|
||||
lex *lexer
|
||||
token [3]item // three-token lookahead for parser.
|
||||
peekCount int
|
||||
vars []string // variables defined at the moment.
|
||||
treeSet map[string]*Tree
|
||||
}
|
||||
|
||||
// Copy returns a copy of the Tree. Any parsing state is discarded.
|
||||
func (t *Tree) Copy() *Tree {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return &Tree{
|
||||
Name: t.Name,
|
||||
ParseName: t.ParseName,
|
||||
Root: t.Root.CopyList(),
|
||||
text: t.text,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns a map from template name to parse.Tree, created by parsing the
|
||||
// templates described in the argument string. The top-level template will be
|
||||
// given the specified name. If an error is encountered, parsing stops and an
|
||||
// empty map is returned with the error.
|
||||
func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]interface{}) (map[string]*Tree, error) {
|
||||
treeSet := make(map[string]*Tree)
|
||||
t := New(name)
|
||||
t.text = text
|
||||
_, err := t.Parse(text, leftDelim, rightDelim, treeSet, funcs...)
|
||||
return treeSet, err
|
||||
}
|
||||
|
||||
// next returns the next token.
|
||||
func (t *Tree) next() item {
|
||||
if t.peekCount > 0 {
|
||||
t.peekCount--
|
||||
} else {
|
||||
t.token[0] = t.lex.nextItem()
|
||||
}
|
||||
return t.token[t.peekCount]
|
||||
}
|
||||
|
||||
// backup backs the input stream up one token.
|
||||
func (t *Tree) backup() {
|
||||
t.peekCount++
|
||||
}
|
||||
|
||||
// backup2 backs the input stream up two tokens.
|
||||
// The zeroth token is already there.
|
||||
func (t *Tree) backup2(t1 item) {
|
||||
t.token[1] = t1
|
||||
t.peekCount = 2
|
||||
}
|
||||
|
||||
// backup3 backs the input stream up three tokens
|
||||
// The zeroth token is already there.
|
||||
func (t *Tree) backup3(t2, t1 item) { // Reverse order: we're pushing back.
|
||||
t.token[1] = t1
|
||||
t.token[2] = t2
|
||||
t.peekCount = 3
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next token.
|
||||
func (t *Tree) peek() item {
|
||||
if t.peekCount > 0 {
|
||||
return t.token[t.peekCount-1]
|
||||
}
|
||||
t.peekCount = 1
|
||||
t.token[0] = t.lex.nextItem()
|
||||
return t.token[0]
|
||||
}
|
||||
|
||||
// nextNonSpace returns the next non-space token.
|
||||
func (t *Tree) nextNonSpace() (token item) {
|
||||
for {
|
||||
token = t.next()
|
||||
if token.typ != itemSpace {
|
||||
break
|
||||
}
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// peekNonSpace returns but does not consume the next non-space token.
|
||||
func (t *Tree) peekNonSpace() (token item) {
|
||||
for {
|
||||
token = t.next()
|
||||
if token.typ != itemSpace {
|
||||
break
|
||||
}
|
||||
}
|
||||
t.backup()
|
||||
return token
|
||||
}
|
||||
|
||||
// Parsing.
|
||||
|
||||
// New allocates a new parse tree with the given name.
|
||||
func New(name string, funcs ...map[string]interface{}) *Tree {
|
||||
return &Tree{
|
||||
Name: name,
|
||||
funcs: funcs,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorContext returns a textual representation of the location of the node in the input text.
|
||||
// The receiver is only used when the node does not have a pointer to the tree inside,
|
||||
// which can occur in old code.
|
||||
func (t *Tree) ErrorContext(n Node) (location, context string) {
|
||||
pos := int(n.Position())
|
||||
tree := n.tree()
|
||||
if tree == nil {
|
||||
tree = t
|
||||
}
|
||||
text := tree.text[:pos]
|
||||
byteNum := strings.LastIndex(text, "\n")
|
||||
if byteNum == -1 {
|
||||
byteNum = pos // On first line.
|
||||
} else {
|
||||
byteNum++ // After the newline.
|
||||
byteNum = pos - byteNum
|
||||
}
|
||||
lineNum := 1 + strings.Count(text, "\n")
|
||||
context = n.String()
|
||||
return fmt.Sprintf("%s:%d:%d", tree.ParseName, lineNum, byteNum), context
|
||||
}
|
||||
|
||||
// errorf formats the error and terminates processing.
|
||||
func (t *Tree) errorf(format string, args ...interface{}) {
|
||||
t.Root = nil
|
||||
format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format)
|
||||
panic(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// error terminates processing.
|
||||
func (t *Tree) error(err error) {
|
||||
t.errorf("%s", err)
|
||||
}
|
||||
|
||||
// expect consumes the next token and guarantees it has the required type.
|
||||
func (t *Tree) expect(expected itemType, context string) item {
|
||||
token := t.nextNonSpace()
|
||||
if token.typ != expected {
|
||||
t.unexpected(token, context)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// expectOneOf consumes the next token and guarantees it has one of the required types.
|
||||
func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item {
|
||||
token := t.nextNonSpace()
|
||||
if token.typ != expected1 && token.typ != expected2 {
|
||||
t.unexpected(token, context)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// unexpected complains about the token and terminates processing.
|
||||
func (t *Tree) unexpected(token item, context string) {
|
||||
t.errorf("unexpected %s in %s", token, context)
|
||||
}
|
||||
|
||||
// recover is the handler that turns panics into returns from the top level of Parse.
|
||||
func (t *Tree) recover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
if _, ok := e.(runtime.Error); ok {
|
||||
panic(e)
|
||||
}
|
||||
if t != nil {
|
||||
t.lex.drain()
|
||||
t.stopParse()
|
||||
}
|
||||
*errp = e.(error)
|
||||
}
|
||||
}
|
||||
|
||||
// startParse initializes the parser, using the lexer.
|
||||
func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer, treeSet map[string]*Tree) {
|
||||
t.Root = nil
|
||||
t.lex = lex
|
||||
t.vars = []string{"$"}
|
||||
t.funcs = funcs
|
||||
t.treeSet = treeSet
|
||||
}
|
||||
|
||||
// stopParse terminates parsing.
|
||||
func (t *Tree) stopParse() {
|
||||
t.lex = nil
|
||||
t.vars = nil
|
||||
t.funcs = nil
|
||||
t.treeSet = nil
|
||||
}
|
||||
|
||||
// Parse parses the template definition string to construct a representation of
|
||||
// the template for execution. If either action delimiter string is empty, the
|
||||
// default ("{{" or "}}") is used. Embedded template definitions are added to
|
||||
// the treeSet map.
|
||||
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
|
||||
defer t.recover(&err)
|
||||
t.ParseName = t.Name
|
||||
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet)
|
||||
t.text = text
|
||||
t.parse()
|
||||
t.add()
|
||||
t.stopParse()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// add adds tree to t.treeSet.
|
||||
func (t *Tree) add() {
|
||||
tree := t.treeSet[t.Name]
|
||||
if tree == nil || IsEmptyTree(tree.Root) {
|
||||
t.treeSet[t.Name] = t
|
||||
return
|
||||
}
|
||||
if !IsEmptyTree(t.Root) {
|
||||
t.errorf("template: multiple definition of template %q", t.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// IsEmptyTree reports whether this tree (node) is empty of everything but space.
|
||||
func IsEmptyTree(n Node) bool {
|
||||
switch n := n.(type) {
|
||||
case nil:
|
||||
return true
|
||||
case *ActionNode:
|
||||
case *IfNode:
|
||||
case *ListNode:
|
||||
for _, node := range n.Nodes {
|
||||
if !IsEmptyTree(node) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *RangeNode:
|
||||
case *TemplateNode:
|
||||
case *TextNode:
|
||||
return len(bytes.TrimSpace(n.Text)) == 0
|
||||
case *WithNode:
|
||||
default:
|
||||
panic("unknown node: " + n.String())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parse is the top-level parser for a template, essentially the same
|
||||
// as itemList except it also parses {{define}} actions.
|
||||
// It runs to EOF.
|
||||
func (t *Tree) parse() {
|
||||
t.Root = t.newList(t.peek().pos)
|
||||
for t.peek().typ != itemEOF {
|
||||
if t.peek().typ == itemLeftDelim {
|
||||
delim := t.next()
|
||||
if t.nextNonSpace().typ == itemDefine {
|
||||
newT := New("definition") // name will be updated once we know it.
|
||||
newT.text = t.text
|
||||
newT.ParseName = t.ParseName
|
||||
newT.startParse(t.funcs, t.lex, t.treeSet)
|
||||
newT.parseDefinition()
|
||||
continue
|
||||
}
|
||||
t.backup2(delim)
|
||||
}
|
||||
switch n := t.textOrAction(); n.Type() {
|
||||
case nodeEnd, nodeElse:
|
||||
t.errorf("unexpected %s", n)
|
||||
default:
|
||||
t.Root.append(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseDefinition parses a {{define}} ... {{end}} template definition and
|
||||
// installs the definition in t.treeSet. The "define" keyword has already
|
||||
// been scanned.
|
||||
func (t *Tree) parseDefinition() {
|
||||
const context = "define clause"
|
||||
name := t.expectOneOf(itemString, itemRawString, context)
|
||||
var err error
|
||||
t.Name, err = strconv.Unquote(name.val)
|
||||
if err != nil {
|
||||
t.error(err)
|
||||
}
|
||||
t.expect(itemRightDelim, context)
|
||||
var end Node
|
||||
t.Root, end = t.itemList()
|
||||
if end.Type() != nodeEnd {
|
||||
t.errorf("unexpected %s in %s", end, context)
|
||||
}
|
||||
t.add()
|
||||
t.stopParse()
|
||||
}
|
||||
|
||||
// itemList:
|
||||
// textOrAction*
|
||||
// Terminates at {{end}} or {{else}}, returned separately.
|
||||
func (t *Tree) itemList() (list *ListNode, next Node) {
|
||||
list = t.newList(t.peekNonSpace().pos)
|
||||
for t.peekNonSpace().typ != itemEOF {
|
||||
n := t.textOrAction()
|
||||
switch n.Type() {
|
||||
case nodeEnd, nodeElse:
|
||||
return list, n
|
||||
}
|
||||
list.append(n)
|
||||
}
|
||||
t.errorf("unexpected EOF")
|
||||
return
|
||||
}
|
||||
|
||||
// textOrAction:
|
||||
// text | action
|
||||
func (t *Tree) textOrAction() Node {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemText:
|
||||
return t.newText(token.pos, token.val)
|
||||
case itemLeftDelim:
|
||||
return t.action()
|
||||
default:
|
||||
t.unexpected(token, "input")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Action:
|
||||
// control
|
||||
// command ("|" command)*
|
||||
// Left delim is past. Now get actions.
|
||||
// First word could be a keyword such as range.
|
||||
func (t *Tree) action() (n Node) {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemBlock:
|
||||
return t.blockControl()
|
||||
case itemElse:
|
||||
return t.elseControl()
|
||||
case itemEnd:
|
||||
return t.endControl()
|
||||
case itemIf:
|
||||
return t.ifControl()
|
||||
case itemRange:
|
||||
return t.rangeControl()
|
||||
case itemTemplate:
|
||||
return t.templateControl()
|
||||
case itemWith:
|
||||
return t.withControl()
|
||||
}
|
||||
t.backup()
|
||||
token := t.peek()
|
||||
// Do not pop variables; they persist until "end".
|
||||
return t.newAction(token.pos, token.line, t.pipeline("command"))
|
||||
}
|
||||
|
||||
// Pipeline:
|
||||
// declarations? command ('|' command)*
|
||||
func (t *Tree) pipeline(context string) (pipe *PipeNode) {
|
||||
token := t.peekNonSpace()
|
||||
pipe = t.newPipeline(token.pos, token.line, nil)
|
||||
// Are there declarations or assignments?
|
||||
decls:
|
||||
if v := t.peekNonSpace(); v.typ == itemVariable {
|
||||
t.next()
|
||||
// Since space is a token, we need 3-token look-ahead here in the worst case:
|
||||
// in "$x foo" we need to read "foo" (as opposed to ":=") to know that $x is an
|
||||
// argument variable rather than a declaration. So remember the token
|
||||
// adjacent to the variable so we can push it back if necessary.
|
||||
tokenAfterVariable := t.peek()
|
||||
next := t.peekNonSpace()
|
||||
switch {
|
||||
case next.typ == itemAssign, next.typ == itemDeclare:
|
||||
pipe.IsAssign = next.typ == itemAssign
|
||||
t.nextNonSpace()
|
||||
pipe.Decl = append(pipe.Decl, t.newVariable(v.pos, v.val))
|
||||
t.vars = append(t.vars, v.val)
|
||||
case next.typ == itemChar && next.val == ",":
|
||||
t.nextNonSpace()
|
||||
pipe.Decl = append(pipe.Decl, t.newVariable(v.pos, v.val))
|
||||
t.vars = append(t.vars, v.val)
|
||||
if context == "range" && len(pipe.Decl) < 2 {
|
||||
switch t.peekNonSpace().typ {
|
||||
case itemVariable, itemRightDelim, itemRightParen:
|
||||
// second initialized variable in a range pipeline
|
||||
goto decls
|
||||
default:
|
||||
t.errorf("range can only initialize variables")
|
||||
}
|
||||
}
|
||||
t.errorf("too many declarations in %s", context)
|
||||
case tokenAfterVariable.typ == itemSpace:
|
||||
t.backup3(v, tokenAfterVariable)
|
||||
default:
|
||||
t.backup2(v)
|
||||
}
|
||||
}
|
||||
for {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemRightDelim, itemRightParen:
|
||||
// At this point, the pipeline is complete
|
||||
t.checkPipeline(pipe, context)
|
||||
if token.typ == itemRightParen {
|
||||
t.backup()
|
||||
}
|
||||
return
|
||||
case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
|
||||
itemNumber, itemNil, itemRawString, itemString, itemVariable, itemLeftParen:
|
||||
t.backup()
|
||||
pipe.append(t.command())
|
||||
default:
|
||||
t.unexpected(token, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
|
||||
// Reject empty pipelines
|
||||
if len(pipe.Cmds) == 0 {
|
||||
t.errorf("missing value for %s", context)
|
||||
}
|
||||
// Only the first command of a pipeline can start with a non executable operand
|
||||
for i, c := range pipe.Cmds[1:] {
|
||||
switch c.Args[0].Type() {
|
||||
case NodeBool, NodeDot, NodeNil, NodeNumber, NodeString:
|
||||
// With A|B|C, pipeline stage 2 is B
|
||||
t.errorf("non executable command in pipeline stage %d", i+2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
|
||||
defer t.popVars(len(t.vars))
|
||||
pipe = t.pipeline(context)
|
||||
var next Node
|
||||
list, next = t.itemList()
|
||||
switch next.Type() {
|
||||
case nodeEnd: //done
|
||||
case nodeElse:
|
||||
if allowElseIf {
|
||||
// Special case for "else if". If the "else" is followed immediately by an "if",
|
||||
// the elseControl will have left the "if" token pending. Treat
|
||||
// {{if a}}_{{else if b}}_{{end}}
|
||||
// as
|
||||
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
|
||||
// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
|
||||
// is assumed. This technique works even for long if-else-if chains.
|
||||
// TODO: Should we allow else-if in with and range?
|
||||
if t.peek().typ == itemIf {
|
||||
t.next() // Consume the "if" token.
|
||||
elseList = t.newList(next.Position())
|
||||
elseList.append(t.ifControl())
|
||||
// Do not consume the next item - only one {{end}} required.
|
||||
break
|
||||
}
|
||||
}
|
||||
elseList, next = t.itemList()
|
||||
if next.Type() != nodeEnd {
|
||||
t.errorf("expected end; found %s", next)
|
||||
}
|
||||
}
|
||||
return pipe.Position(), pipe.Line, pipe, list, elseList
|
||||
}
|
||||
|
||||
// If:
|
||||
// {{if pipeline}} itemList {{end}}
|
||||
// {{if pipeline}} itemList {{else}} itemList {{end}}
|
||||
// If keyword is past.
|
||||
func (t *Tree) ifControl() Node {
|
||||
return t.newIf(t.parseControl(true, "if"))
|
||||
}
|
||||
|
||||
// Range:
|
||||
// {{range pipeline}} itemList {{end}}
|
||||
// {{range pipeline}} itemList {{else}} itemList {{end}}
|
||||
// Range keyword is past.
|
||||
func (t *Tree) rangeControl() Node {
|
||||
return t.newRange(t.parseControl(false, "range"))
|
||||
}
|
||||
|
||||
// With:
|
||||
// {{with pipeline}} itemList {{end}}
|
||||
// {{with pipeline}} itemList {{else}} itemList {{end}}
|
||||
// If keyword is past.
|
||||
func (t *Tree) withControl() Node {
|
||||
return t.newWith(t.parseControl(false, "with"))
|
||||
}
|
||||
|
||||
// End:
|
||||
// {{end}}
|
||||
// End keyword is past.
|
||||
func (t *Tree) endControl() Node {
|
||||
return t.newEnd(t.expect(itemRightDelim, "end").pos)
|
||||
}
|
||||
|
||||
// Else:
|
||||
// {{else}}
|
||||
// Else keyword is past.
|
||||
func (t *Tree) elseControl() Node {
|
||||
// Special case for "else if".
|
||||
peek := t.peekNonSpace()
|
||||
if peek.typ == itemIf {
|
||||
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
|
||||
return t.newElse(peek.pos, peek.line)
|
||||
}
|
||||
token := t.expect(itemRightDelim, "else")
|
||||
return t.newElse(token.pos, token.line)
|
||||
}
|
||||
|
||||
// Block:
|
||||
// {{block stringValue pipeline}}
|
||||
// Block keyword is past.
|
||||
// The name must be something that can evaluate to a string.
|
||||
// The pipeline is mandatory.
|
||||
func (t *Tree) blockControl() Node {
|
||||
const context = "block clause"
|
||||
|
||||
token := t.nextNonSpace()
|
||||
name := t.parseTemplateName(token, context)
|
||||
pipe := t.pipeline(context)
|
||||
|
||||
block := New(name) // name will be updated once we know it.
|
||||
block.text = t.text
|
||||
block.ParseName = t.ParseName
|
||||
block.startParse(t.funcs, t.lex, t.treeSet)
|
||||
var end Node
|
||||
block.Root, end = block.itemList()
|
||||
if end.Type() != nodeEnd {
|
||||
t.errorf("unexpected %s in %s", end, context)
|
||||
}
|
||||
block.add()
|
||||
block.stopParse()
|
||||
|
||||
return t.newTemplate(token.pos, token.line, name, pipe)
|
||||
}
|
||||
|
||||
// Template:
|
||||
// {{template stringValue pipeline}}
|
||||
// Template keyword is past. The name must be something that can evaluate
|
||||
// to a string.
|
||||
func (t *Tree) templateControl() Node {
|
||||
const context = "template clause"
|
||||
token := t.nextNonSpace()
|
||||
name := t.parseTemplateName(token, context)
|
||||
var pipe *PipeNode
|
||||
if t.nextNonSpace().typ != itemRightDelim {
|
||||
t.backup()
|
||||
// Do not pop variables; they persist until "end".
|
||||
pipe = t.pipeline(context)
|
||||
}
|
||||
return t.newTemplate(token.pos, token.line, name, pipe)
|
||||
}
|
||||
|
||||
func (t *Tree) parseTemplateName(token item, context string) (name string) {
|
||||
switch token.typ {
|
||||
case itemString, itemRawString:
|
||||
s, err := strconv.Unquote(token.val)
|
||||
if err != nil {
|
||||
t.error(err)
|
||||
}
|
||||
name = s
|
||||
default:
|
||||
t.unexpected(token, context)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// command:
|
||||
// operand (space operand)*
|
||||
// space-separated arguments up to a pipeline character or right delimiter.
|
||||
// we consume the pipe character but leave the right delim to terminate the action.
|
||||
func (t *Tree) command() *CommandNode {
|
||||
cmd := t.newCommand(t.peekNonSpace().pos)
|
||||
for {
|
||||
t.peekNonSpace() // skip leading spaces.
|
||||
operand := t.operand()
|
||||
if operand != nil {
|
||||
cmd.append(operand)
|
||||
}
|
||||
switch token := t.next(); token.typ {
|
||||
case itemSpace:
|
||||
continue
|
||||
case itemError:
|
||||
t.errorf("%s", token.val)
|
||||
case itemRightDelim, itemRightParen:
|
||||
t.backup()
|
||||
case itemPipe:
|
||||
default:
|
||||
t.errorf("unexpected %s in operand", token)
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(cmd.Args) == 0 {
|
||||
t.errorf("empty command")
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// operand:
|
||||
// term .Field*
|
||||
// An operand is a space-separated component of a command,
|
||||
// a term possibly followed by field accesses.
|
||||
// A nil return means the next item is not an operand.
|
||||
func (t *Tree) operand() Node {
|
||||
node := t.term()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if t.peek().typ == itemField {
|
||||
chain := t.newChain(t.peek().pos, node)
|
||||
for t.peek().typ == itemField {
|
||||
chain.Add(t.next().val)
|
||||
}
|
||||
// Compatibility with original API: If the term is of type NodeField
|
||||
// or NodeVariable, just put more fields on the original.
|
||||
// Otherwise, keep the Chain node.
|
||||
// Obvious parsing errors involving literal values are detected here.
|
||||
// More complex error cases will have to be handled at execution time.
|
||||
switch node.Type() {
|
||||
case NodeField:
|
||||
node = t.newField(chain.Position(), chain.String())
|
||||
case NodeVariable:
|
||||
node = t.newVariable(chain.Position(), chain.String())
|
||||
case NodeBool, NodeString, NodeNumber, NodeNil, NodeDot:
|
||||
t.errorf("unexpected . after term %q", node.String())
|
||||
default:
|
||||
node = chain
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// term:
|
||||
// literal (number, string, nil, boolean)
|
||||
// function (identifier)
|
||||
// .
|
||||
// .Field
|
||||
// $
|
||||
// '(' pipeline ')'
|
||||
// A term is a simple "expression".
|
||||
// A nil return means the next item is not a term.
|
||||
func (t *Tree) term() Node {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemError:
|
||||
t.errorf("%s", token.val)
|
||||
case itemIdentifier:
|
||||
if !t.hasFunction(token.val) {
|
||||
t.errorf("function %q not defined", token.val)
|
||||
}
|
||||
return NewIdentifier(token.val).SetTree(t).SetPos(token.pos)
|
||||
case itemDot:
|
||||
return t.newDot(token.pos)
|
||||
case itemNil:
|
||||
return t.newNil(token.pos)
|
||||
case itemVariable:
|
||||
return t.useVar(token.pos, token.val)
|
||||
case itemField:
|
||||
return t.newField(token.pos, token.val)
|
||||
case itemBool:
|
||||
return t.newBool(token.pos, token.val == "true")
|
||||
case itemCharConstant, itemComplex, itemNumber:
|
||||
number, err := t.newNumber(token.pos, token.val, token.typ)
|
||||
if err != nil {
|
||||
t.error(err)
|
||||
}
|
||||
return number
|
||||
case itemLeftParen:
|
||||
pipe := t.pipeline("parenthesized pipeline")
|
||||
if token := t.next(); token.typ != itemRightParen {
|
||||
t.errorf("unclosed right paren: unexpected %s", token)
|
||||
}
|
||||
return pipe
|
||||
case itemString, itemRawString:
|
||||
s, err := strconv.Unquote(token.val)
|
||||
if err != nil {
|
||||
t.error(err)
|
||||
}
|
||||
return t.newString(token.pos, token.val, s)
|
||||
}
|
||||
t.backup()
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasFunction reports if a function name exists in the Tree's maps.
|
||||
func (t *Tree) hasFunction(name string) bool {
|
||||
for _, funcMap := range t.funcs {
|
||||
if funcMap == nil {
|
||||
continue
|
||||
}
|
||||
if funcMap[name] != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// popVars trims the variable list to the specified length
|
||||
func (t *Tree) popVars(n int) {
|
||||
t.vars = t.vars[:n]
|
||||
}
|
||||
|
||||
// useVar returns a node for a variable reference. It errors if the
|
||||
// variable is not defined.
|
||||
func (t *Tree) useVar(pos Pos, name string) Node {
|
||||
v := t.newVariable(pos, name)
|
||||
for _, varName := range t.vars {
|
||||
if varName == v.Ident[0] {
|
||||
return v
|
||||
}
|
||||
}
|
||||
t.errorf("undefined variable %q", v.Ident[0])
|
||||
return nil
|
||||
}
|
557
tpl/internal/go_templates/texttemplate/parse/parse_test.go
Normal file
557
tpl/internal/go_templates/texttemplate/parse/parse_test.go
Normal file
|
@ -0,0 +1,557 @@
|
|||
// Copyright 2011 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 go1.13
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var debug = flag.Bool("debug", false, "show the errors produced by the main tests")
|
||||
|
||||
type numberTest struct {
|
||||
text string
|
||||
isInt bool
|
||||
isUint bool
|
||||
isFloat bool
|
||||
isComplex bool
|
||||
int64
|
||||
uint64
|
||||
float64
|
||||
complex128
|
||||
}
|
||||
|
||||
var numberTests = []numberTest{
|
||||
// basics
|
||||
{"0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"-0", true, true, true, false, 0, 0, 0, 0}, // check that -0 is a uint.
|
||||
{"73", true, true, true, false, 73, 73, 73, 0},
|
||||
{"7_3", true, true, true, false, 73, 73, 73, 0},
|
||||
{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||
{"0B10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0o73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0O73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"-73", true, false, true, false, -73, 0, -73, 0},
|
||||
{"+73", true, false, true, false, 73, 0, 73, 0},
|
||||
{"100", true, true, true, false, 100, 100, 100, 0},
|
||||
{"1e9", true, true, true, false, 1e9, 1e9, 1e9, 0},
|
||||
{"-1e9", true, false, true, false, -1e9, 0, -1e9, 0},
|
||||
{"-1.2", false, false, true, false, 0, 0, -1.2, 0},
|
||||
{"1e19", false, true, true, false, 0, 1e19, 1e19, 0},
|
||||
{"1e1_9", false, true, true, false, 0, 1e19, 1e19, 0},
|
||||
{"1E19", false, true, true, false, 0, 1e19, 1e19, 0},
|
||||
{"-1e19", false, false, true, false, 0, 0, -1e19, 0},
|
||||
{"0x_1p4", true, true, true, false, 16, 16, 16, 0},
|
||||
{"0X_1P4", true, true, true, false, 16, 16, 16, 0},
|
||||
{"0x_1p-4", false, false, true, false, 0, 0, 1 / 16., 0},
|
||||
{"4i", false, false, false, true, 0, 0, 0, 4i},
|
||||
{"-1.2+4.2i", false, false, false, true, 0, 0, 0, -1.2 + 4.2i},
|
||||
{"073i", false, false, false, true, 0, 0, 0, 73i}, // not octal!
|
||||
// complex with 0 imaginary are float (and maybe integer)
|
||||
{"0i", true, true, true, true, 0, 0, 0, 0},
|
||||
{"-1.2+0i", false, false, true, true, 0, 0, -1.2, -1.2},
|
||||
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||
// funny bases
|
||||
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||
// character constants
|
||||
{`'a'`, true, true, true, false, 'a', 'a', 'a', 0},
|
||||
{`'\n'`, true, true, true, false, '\n', '\n', '\n', 0},
|
||||
{`'\\'`, true, true, true, false, '\\', '\\', '\\', 0},
|
||||
{`'\''`, true, true, true, false, '\'', '\'', '\'', 0},
|
||||
{`'\xFF'`, true, true, true, false, 0xFF, 0xFF, 0xFF, 0},
|
||||
{`'パ'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
{`'\u30d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
{`'\U000030d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
// some broken syntax
|
||||
{text: "+-2"},
|
||||
{text: "0x123."},
|
||||
{text: "1e."},
|
||||
{text: "0xi."},
|
||||
{text: "1+2."},
|
||||
{text: "'x"},
|
||||
{text: "'xx'"},
|
||||
{text: "'433937734937734969526500969526500'"}, // Integer too large - issue 10634.
|
||||
// Issue 8622 - 0xe parsed as floating point. Very embarrassing.
|
||||
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
|
||||
}
|
||||
|
||||
func TestNumberParse(t *testing.T) {
|
||||
for _, test := range numberTests {
|
||||
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
|
||||
// because imaginary comes out as a number.
|
||||
var c complex128
|
||||
typ := itemNumber
|
||||
var tree *Tree
|
||||
if test.text[0] == '\'' {
|
||||
typ = itemCharConstant
|
||||
} else {
|
||||
_, err := fmt.Sscan(test.text, &c)
|
||||
if err == nil {
|
||||
typ = itemComplex
|
||||
}
|
||||
}
|
||||
n, err := tree.newNumber(0, test.text, typ)
|
||||
ok := test.isInt || test.isUint || test.isFloat || test.isComplex
|
||||
if ok && err != nil {
|
||||
t.Errorf("unexpected error for %q: %s", test.text, err)
|
||||
continue
|
||||
}
|
||||
if !ok && err == nil {
|
||||
t.Errorf("expected error for %q", test.text)
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
if *debug {
|
||||
fmt.Printf("%s\n\t%s\n", test.text, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if n.IsComplex != test.isComplex {
|
||||
t.Errorf("complex incorrect for %q; should be %t", test.text, test.isComplex)
|
||||
}
|
||||
if test.isInt {
|
||||
if !n.IsInt {
|
||||
t.Errorf("expected integer for %q", test.text)
|
||||
}
|
||||
if n.Int64 != test.int64 {
|
||||
t.Errorf("int64 for %q should be %d Is %d", test.text, test.int64, n.Int64)
|
||||
}
|
||||
} else if n.IsInt {
|
||||
t.Errorf("did not expect integer for %q", test.text)
|
||||
}
|
||||
if test.isUint {
|
||||
if !n.IsUint {
|
||||
t.Errorf("expected unsigned integer for %q", test.text)
|
||||
}
|
||||
if n.Uint64 != test.uint64 {
|
||||
t.Errorf("uint64 for %q should be %d Is %d", test.text, test.uint64, n.Uint64)
|
||||
}
|
||||
} else if n.IsUint {
|
||||
t.Errorf("did not expect unsigned integer for %q", test.text)
|
||||
}
|
||||
if test.isFloat {
|
||||
if !n.IsFloat {
|
||||
t.Errorf("expected float for %q", test.text)
|
||||
}
|
||||
if n.Float64 != test.float64 {
|
||||
t.Errorf("float64 for %q should be %g Is %g", test.text, test.float64, n.Float64)
|
||||
}
|
||||
} else if n.IsFloat {
|
||||
t.Errorf("did not expect float for %q", test.text)
|
||||
}
|
||||
if test.isComplex {
|
||||
if !n.IsComplex {
|
||||
t.Errorf("expected complex for %q", test.text)
|
||||
}
|
||||
if n.Complex128 != test.complex128 {
|
||||
t.Errorf("complex128 for %q should be %g Is %g", test.text, test.complex128, n.Complex128)
|
||||
}
|
||||
} else if n.IsComplex {
|
||||
t.Errorf("did not expect complex for %q", test.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type parseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
result string // what the user would see in an error message.
|
||||
}
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
var parseTests = []parseTest{
|
||||
{"empty", "", noError,
|
||||
``},
|
||||
{"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``},
|
||||
{"spaces", " \t\n", noError,
|
||||
`" \t\n"`},
|
||||
{"text", "some text", noError,
|
||||
`"some text"`},
|
||||
{"emptyAction", "{{}}", hasError,
|
||||
`{{}}`},
|
||||
{"field", "{{.X}}", noError,
|
||||
`{{.X}}`},
|
||||
{"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`},
|
||||
{"$ invocation", "{{$}}", noError,
|
||||
"{{$}}"},
|
||||
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
||||
{"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}"},
|
||||
{"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}"},
|
||||
{"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`},
|
||||
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`},
|
||||
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
||||
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`},
|
||||
{"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`},
|
||||
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
|
||||
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
|
||||
{"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`},
|
||||
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`},
|
||||
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
||||
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
||||
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`},
|
||||
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`},
|
||||
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
||||
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
||||
{"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`},
|
||||
{"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`},
|
||||
{"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`},
|
||||
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
|
||||
// Trimming spaces.
|
||||
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
|
||||
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
|
||||
{"trim left and right", "x \r\n\t{{- 3 -}}\n\n\ty", noError, `"x"{{3}}"y"`},
|
||||
{"trim with extra spaces", "x\n{{- 3 -}}\ny", noError, `"x"{{3}}"y"`},
|
||||
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
|
||||
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
||||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
|
||||
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||
`{{template "foo" .}}`},
|
||||
// Errors.
|
||||
{"unclosed action", "hello{{range", hasError, ""},
|
||||
{"unmatched end", "{{end}}", hasError, ""},
|
||||
{"unmatched else", "{{else}}", hasError, ""},
|
||||
{"unmatched else after if", "{{if .X}}hello{{end}}{{else}}", hasError, ""},
|
||||
{"multiple else", "{{if .X}}1{{else}}2{{else}}3{{end}}", hasError, ""},
|
||||
{"missing end", "hello{{range .x}}", hasError, ""},
|
||||
{"missing end after else", "hello{{range .x}}{{else}}", hasError, ""},
|
||||
{"undefined function", "hello{{undefined}}", hasError, ""},
|
||||
{"undefined variable", "{{$x}}", hasError, ""},
|
||||
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
|
||||
{"variable undefined in template", "{{template $v}}", hasError, ""},
|
||||
{"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
|
||||
{"template with field ref", "{{template .X}}", hasError, ""},
|
||||
{"template with var", "{{template $v}}", hasError, ""},
|
||||
{"invalid punctuation", "{{printf 3, 4}}", hasError, ""},
|
||||
{"multidecl outside range", "{{with $v, $u := 3}}{{end}}", hasError, ""},
|
||||
{"too many decls in range", "{{range $u, $v, $w := 3}}{{end}}", hasError, ""},
|
||||
{"dot applied to parentheses", "{{printf (printf .).}}", hasError, ""},
|
||||
{"adjacent args", "{{printf 3`x`}}", hasError, ""},
|
||||
{"adjacent args with .", "{{printf `x`.}}", hasError, ""},
|
||||
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
|
||||
// Other kinds of assignments and operators aren't available yet.
|
||||
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
|
||||
{"bug0b", "{{$x += 1}}{{$x}}", hasError, ""},
|
||||
{"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
|
||||
{"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
|
||||
// Check the parse fails for := rather than comma.
|
||||
{"bug0e", "{{range $x := $y := 3}}{{end}}", hasError, ""},
|
||||
// Another bug: variable read must ignore following punctuation.
|
||||
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
||||
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
|
||||
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
|
||||
// dot following a literal value
|
||||
{"dot after integer", "{{1.E}}", hasError, ""},
|
||||
{"dot after float", "{{0.1.E}}", hasError, ""},
|
||||
{"dot after boolean", "{{true.E}}", hasError, ""},
|
||||
{"dot after char", "{{'a'.any}}", hasError, ""},
|
||||
{"dot after string", `{{"hello".guys}}`, hasError, ""},
|
||||
{"dot after dot", "{{..E}}", hasError, ""},
|
||||
{"dot after nil", "{{nil.E}}", hasError, ""},
|
||||
// Wrong pipeline
|
||||
{"wrong pipeline dot", "{{12|.}}", hasError, ""},
|
||||
{"wrong pipeline number", "{{.|12|printf}}", hasError, ""},
|
||||
{"wrong pipeline string", "{{.|printf|\"error\"}}", hasError, ""},
|
||||
{"wrong pipeline char", "{{12|printf|'e'}}", hasError, ""},
|
||||
{"wrong pipeline boolean", "{{.|true}}", hasError, ""},
|
||||
{"wrong pipeline nil", "{{'c'|nil}}", hasError, ""},
|
||||
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
|
||||
// Missing pipeline in block
|
||||
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
|
||||
}
|
||||
|
||||
var builtins = map[string]interface{}{
|
||||
"printf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
func testParse(doCopy bool, t *testing.T) {
|
||||
textFormat = "%q"
|
||||
defer func() { textFormat = "%s" }()
|
||||
for _, test := range parseTests {
|
||||
tmpl, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree), builtins)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var result string
|
||||
if doCopy {
|
||||
result = tmpl.Root.Copy().String()
|
||||
} else {
|
||||
result = tmpl.Root.String()
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testParse(false, t)
|
||||
}
|
||||
|
||||
// Same as TestParse, but we copy the node first
|
||||
func TestParseCopy(t *testing.T) {
|
||||
testParse(true, t)
|
||||
}
|
||||
|
||||
type isEmptyTest struct {
|
||||
name string
|
||||
input string
|
||||
empty bool
|
||||
}
|
||||
|
||||
var isEmptyTests = []isEmptyTest{
|
||||
{"empty", ``, true},
|
||||
{"nonempty", `hello`, false},
|
||||
{"spaces only", " \t\n \t\n", true},
|
||||
{"definition", `{{define "x"}}something{{end}}`, true},
|
||||
{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
|
||||
{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n", false},
|
||||
{"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
|
||||
}
|
||||
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
if !IsEmptyTree(nil) {
|
||||
t.Errorf("nil tree is not empty")
|
||||
}
|
||||
for _, test := range isEmptyTests {
|
||||
tree, err := New("root").Parse(test.input, "", "", make(map[string]*Tree), nil)
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
if empty := IsEmptyTree(tree.Root); empty != test.empty {
|
||||
t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorContextWithTreeCopy(t *testing.T) {
|
||||
tree, err := New("root").Parse("{{if true}}{{end}}", "", "", make(map[string]*Tree), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected tree parse failure: %v", err)
|
||||
}
|
||||
treeCopy := tree.Copy()
|
||||
wantLocation, wantContext := tree.ErrorContext(tree.Root.Nodes[0])
|
||||
gotLocation, gotContext := treeCopy.ErrorContext(treeCopy.Root.Nodes[0])
|
||||
if wantLocation != gotLocation {
|
||||
t.Errorf("wrong error location want %q got %q", wantLocation, gotLocation)
|
||||
}
|
||||
if wantContext != gotContext {
|
||||
t.Errorf("wrong error location want %q got %q", wantContext, gotContext)
|
||||
}
|
||||
}
|
||||
|
||||
// All failures, and the result is a string that must appear in the error message.
|
||||
var errorTests = []parseTest{
|
||||
// Check line numbers are accurate.
|
||||
{"unclosed1",
|
||||
"line1\n{{",
|
||||
hasError, `unclosed1:2: unexpected unclosed action in command`},
|
||||
{"unclosed2",
|
||||
"line1\n{{define `x`}}line2\n{{",
|
||||
hasError, `unclosed2:3: unexpected unclosed action in command`},
|
||||
// Specific errors.
|
||||
{"function",
|
||||
"{{foo}}",
|
||||
hasError, `function "foo" not defined`},
|
||||
{"comment",
|
||||
"{{/*}}",
|
||||
hasError, `unclosed comment`},
|
||||
{"lparen",
|
||||
"{{.X (1 2 3}}",
|
||||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
"{{.X 1 2 3)}}",
|
||||
hasError, `unexpected ")"`},
|
||||
{"space",
|
||||
"{{`x`3}}",
|
||||
hasError, `in operand`},
|
||||
{"idchar",
|
||||
"{{a#}}",
|
||||
hasError, `'#'`},
|
||||
{"charconst",
|
||||
"{{'a}}",
|
||||
hasError, `unterminated character constant`},
|
||||
{"stringconst",
|
||||
`{{"a}}`,
|
||||
hasError, `unterminated quoted string`},
|
||||
{"rawstringconst",
|
||||
"{{`a}}",
|
||||
hasError, `unterminated raw quoted string`},
|
||||
{"number",
|
||||
"{{0xi}}",
|
||||
hasError, `number syntax`},
|
||||
{"multidefine",
|
||||
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||
hasError, `multiple definition of template`},
|
||||
{"eof",
|
||||
"{{range .X}}",
|
||||
hasError, `unexpected EOF`},
|
||||
{"variable",
|
||||
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
||||
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||
hasError, `unexpected ":="`},
|
||||
{"multidecl",
|
||||
"{{$a,$b,$c := 23}}",
|
||||
hasError, `too many declarations`},
|
||||
{"undefvar",
|
||||
"{{$a}}",
|
||||
hasError, `undefined variable`},
|
||||
{"wrongdot",
|
||||
"{{true.any}}",
|
||||
hasError, `unexpected . after term`},
|
||||
{"wrongpipeline",
|
||||
"{{12|false}}",
|
||||
hasError, `non executable command in pipeline`},
|
||||
{"emptypipeline",
|
||||
`{{ ( ) }}`,
|
||||
hasError, `missing value for parenthesized pipeline`},
|
||||
{"multilinerawstring",
|
||||
"{{ $v := `\n` }} {{",
|
||||
hasError, `multilinerawstring:2: unexpected unclosed action`},
|
||||
{"rangeundefvar",
|
||||
"{{range $k}}{{end}}",
|
||||
hasError, `undefined variable`},
|
||||
{"rangeundefvars",
|
||||
"{{range $k, $v}}{{end}}",
|
||||
hasError, `undefined variable`},
|
||||
{"rangemissingvalue1",
|
||||
"{{range $k,}}{{end}}",
|
||||
hasError, `missing value for range`},
|
||||
{"rangemissingvalue2",
|
||||
"{{range $k, $v := }}{{end}}",
|
||||
hasError, `missing value for range`},
|
||||
{"rangenotvariable1",
|
||||
"{{range $k, .}}{{end}}",
|
||||
hasError, `range can only initialize variables`},
|
||||
{"rangenotvariable2",
|
||||
"{{range $k, 123 := .}}{{end}}",
|
||||
hasError, `range can only initialize variables`},
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
for _, test := range errorTests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error %q, got nil", test.result)
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.result) {
|
||||
t.Fatalf("error %q does not contain %q", err, test.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlock(t *testing.T) {
|
||||
const (
|
||||
input = `a{{block "inner" .}}bar{{.}}baz{{end}}b`
|
||||
outer = `a{{template "inner" .}}b`
|
||||
inner = `bar{{.}}baz`
|
||||
)
|
||||
treeSet := make(map[string]*Tree)
|
||||
tmpl, err := New("outer").Parse(input, "", "", treeSet, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := tmpl.Root.String(), outer; g != w {
|
||||
t.Errorf("outer template = %q, want %q", g, w)
|
||||
}
|
||||
inTmpl := treeSet["inner"]
|
||||
if inTmpl == nil {
|
||||
t.Fatal("block did not define template")
|
||||
}
|
||||
if g, w := inTmpl.Root.String(), inner; g != w {
|
||||
t.Errorf("inner template = %q, want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineNum(t *testing.T) {
|
||||
const count = 100
|
||||
text := strings.Repeat("{{printf 1234}}\n", count)
|
||||
tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Check the line numbers. Each line is an action containing a template, followed by text.
|
||||
// That's two nodes per line.
|
||||
nodes := tree.Root.Nodes
|
||||
for i := 0; i < len(nodes); i += 2 {
|
||||
line := 1 + i/2
|
||||
// Action first.
|
||||
action := nodes[i].(*ActionNode)
|
||||
if action.Line != line {
|
||||
t.Fatalf("line %d: action is line %d", line, action.Line)
|
||||
}
|
||||
pipe := action.Pipe
|
||||
if pipe.Line != line {
|
||||
t.Fatalf("line %d: pipe is line %d", line, pipe.Line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseLarge(b *testing.B) {
|
||||
text := strings.Repeat("{{1234}}\n", 10000)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
228
tpl/internal/go_templates/texttemplate/template.go
Normal file
228
tpl/internal/go_templates/texttemplate/template.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
// Copyright 2011 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 template
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// common holds the information shared by related templates.
|
||||
type common struct {
|
||||
tmpl map[string]*Template // Map from name to defined templates.
|
||||
option option
|
||||
// We use two maps, one for parsing and one for execution.
|
||||
// This separation makes the API cleaner since it doesn't
|
||||
// expose reflection to the client.
|
||||
muFuncs sync.RWMutex // protects parseFuncs and execFuncs
|
||||
parseFuncs FuncMap
|
||||
execFuncs map[string]reflect.Value
|
||||
}
|
||||
|
||||
// Template is the representation of a parsed template. The *parse.Tree
|
||||
// field is exported only for use by html/template and should be treated
|
||||
// as unexported by all other clients.
|
||||
type Template struct {
|
||||
name string
|
||||
*parse.Tree
|
||||
*common
|
||||
leftDelim string
|
||||
rightDelim string
|
||||
}
|
||||
|
||||
// New allocates a new, undefined template with the given name.
|
||||
func New(name string) *Template {
|
||||
t := &Template{
|
||||
name: name,
|
||||
}
|
||||
t.init()
|
||||
return t
|
||||
}
|
||||
|
||||
// Name returns the name of the template.
|
||||
func (t *Template) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
// New allocates a new, undefined template associated with the given one and with the same
|
||||
// delimiters. The association, which is transitive, allows one template to
|
||||
// invoke another with a {{template}} action.
|
||||
//
|
||||
// Because associated templates share underlying data, template construction
|
||||
// cannot be done safely in parallel. Once the templates are constructed, they
|
||||
// can be executed in parallel.
|
||||
func (t *Template) New(name string) *Template {
|
||||
t.init()
|
||||
nt := &Template{
|
||||
name: name,
|
||||
common: t.common,
|
||||
leftDelim: t.leftDelim,
|
||||
rightDelim: t.rightDelim,
|
||||
}
|
||||
return nt
|
||||
}
|
||||
|
||||
// init guarantees that t has a valid common structure.
|
||||
func (t *Template) init() {
|
||||
if t.common == nil {
|
||||
c := new(common)
|
||||
c.tmpl = make(map[string]*Template)
|
||||
c.parseFuncs = make(FuncMap)
|
||||
c.execFuncs = make(map[string]reflect.Value)
|
||||
t.common = c
|
||||
}
|
||||
}
|
||||
|
||||
// Clone returns a duplicate of the template, including all associated
|
||||
// templates. The actual representation is not copied, but the name space of
|
||||
// associated templates is, so further calls to Parse in the copy will add
|
||||
// templates to the copy but not to the original. Clone can be used to prepare
|
||||
// common templates and use them with variant definitions for other templates
|
||||
// by adding the variants after the clone is made.
|
||||
func (t *Template) Clone() (*Template, error) {
|
||||
nt := t.copy(nil)
|
||||
nt.init()
|
||||
if t.common == nil {
|
||||
return nt, nil
|
||||
}
|
||||
for k, v := range t.tmpl {
|
||||
if k == t.name {
|
||||
nt.tmpl[t.name] = nt
|
||||
continue
|
||||
}
|
||||
// The associated templates share nt's common structure.
|
||||
tmpl := v.copy(nt.common)
|
||||
nt.tmpl[k] = tmpl
|
||||
}
|
||||
t.muFuncs.RLock()
|
||||
defer t.muFuncs.RUnlock()
|
||||
for k, v := range t.parseFuncs {
|
||||
nt.parseFuncs[k] = v
|
||||
}
|
||||
for k, v := range t.execFuncs {
|
||||
nt.execFuncs[k] = v
|
||||
}
|
||||
return nt, nil
|
||||
}
|
||||
|
||||
// copy returns a shallow copy of t, with common set to the argument.
|
||||
func (t *Template) copy(c *common) *Template {
|
||||
nt := New(t.name)
|
||||
nt.Tree = t.Tree
|
||||
nt.common = c
|
||||
nt.leftDelim = t.leftDelim
|
||||
nt.rightDelim = t.rightDelim
|
||||
return nt
|
||||
}
|
||||
|
||||
// AddParseTree adds parse tree for template with given name and associates it with t.
|
||||
// If the template does not already exist, it will create a new one.
|
||||
// If the template does exist, it will be replaced.
|
||||
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
|
||||
t.init()
|
||||
// If the name is the name of this template, overwrite this template.
|
||||
nt := t
|
||||
if name != t.name {
|
||||
nt = t.New(name)
|
||||
}
|
||||
// Even if nt == t, we need to install it in the common.tmpl map.
|
||||
if t.associate(nt, tree) || nt.Tree == nil {
|
||||
nt.Tree = tree
|
||||
}
|
||||
return nt, nil
|
||||
}
|
||||
|
||||
// Templates returns a slice of defined templates associated with t.
|
||||
func (t *Template) Templates() []*Template {
|
||||
if t.common == nil {
|
||||
return nil
|
||||
}
|
||||
// Return a slice so we don't expose the map.
|
||||
m := make([]*Template, 0, len(t.tmpl))
|
||||
for _, v := range t.tmpl {
|
||||
m = append(m, v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Delims sets the action delimiters to the specified strings, to be used in
|
||||
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
|
||||
// definitions will inherit the settings. An empty delimiter stands for the
|
||||
// corresponding default: {{ or }}.
|
||||
// The return value is the template, so calls can be chained.
|
||||
func (t *Template) Delims(left, right string) *Template {
|
||||
t.init()
|
||||
t.leftDelim = left
|
||||
t.rightDelim = right
|
||||
return t
|
||||
}
|
||||
|
||||
// Funcs adds the elements of the argument map to the template's function map.
|
||||
// It must be called before the template is parsed.
|
||||
// It panics if a value in the map is not a function with appropriate return
|
||||
// type or if the name cannot be used syntactically as a function in a template.
|
||||
// It is legal to overwrite elements of the map. The return value is the template,
|
||||
// so calls can be chained.
|
||||
func (t *Template) Funcs(funcMap FuncMap) *Template {
|
||||
t.init()
|
||||
t.muFuncs.Lock()
|
||||
defer t.muFuncs.Unlock()
|
||||
addValueFuncs(t.execFuncs, funcMap)
|
||||
addFuncs(t.parseFuncs, funcMap)
|
||||
return t
|
||||
}
|
||||
|
||||
// Lookup returns the template with the given name that is associated with t.
|
||||
// It returns nil if there is no such template or the template has no definition.
|
||||
func (t *Template) Lookup(name string) *Template {
|
||||
if t.common == nil {
|
||||
return nil
|
||||
}
|
||||
return t.tmpl[name]
|
||||
}
|
||||
|
||||
// Parse parses text as a template body for t.
|
||||
// Named template definitions ({{define ...}} or {{block ...}} statements) in text
|
||||
// define additional templates associated with t and are removed from the
|
||||
// definition of t itself.
|
||||
//
|
||||
// Templates can be redefined in successive calls to Parse.
|
||||
// A template definition with a body containing only white space and comments
|
||||
// is considered empty and will not replace an existing template's body.
|
||||
// This allows using Parse to add new named template definitions without
|
||||
// overwriting the main template body.
|
||||
func (t *Template) Parse(text string) (*Template, error) {
|
||||
t.init()
|
||||
t.muFuncs.RLock()
|
||||
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins)
|
||||
t.muFuncs.RUnlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Add the newly parsed trees, including the one for t, into our common structure.
|
||||
for name, tree := range trees {
|
||||
if _, err := t.AddParseTree(name, tree); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// associate installs the new template into the group of templates associated
|
||||
// with t. The two are already known to share the common structure.
|
||||
// The boolean return value reports whether to store this tree as t.Tree.
|
||||
func (t *Template) associate(new *Template, tree *parse.Tree) bool {
|
||||
if new.common != t.common {
|
||||
panic("internal error: associate not common")
|
||||
}
|
||||
if old := t.tmpl[new.name]; old != nil && parse.IsEmptyTree(tree.Root) && old.Tree != nil {
|
||||
// If a template by that name exists,
|
||||
// don't replace it with an empty template.
|
||||
return false
|
||||
}
|
||||
t.tmpl[new.name] = new
|
||||
return true
|
||||
}
|
2
tpl/internal/go_templates/texttemplate/testdata/file1.tmpl
vendored
Normal file
2
tpl/internal/go_templates/texttemplate/testdata/file1.tmpl
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{{define "x"}}TEXT{{end}}
|
||||
{{define "dotV"}}{{.V}}{{end}}
|
2
tpl/internal/go_templates/texttemplate/testdata/file2.tmpl
vendored
Normal file
2
tpl/internal/go_templates/texttemplate/testdata/file2.tmpl
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{{define "dot"}}{{.}}{{end}}
|
||||
{{define "nested"}}{{template "dot" .}}{{end}}
|
3
tpl/internal/go_templates/texttemplate/testdata/tmpl1.tmpl
vendored
Normal file
3
tpl/internal/go_templates/texttemplate/testdata/tmpl1.tmpl
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
template1
|
||||
{{define "x"}}x{{end}}
|
||||
{{template "y"}}
|
3
tpl/internal/go_templates/texttemplate/testdata/tmpl2.tmpl
vendored
Normal file
3
tpl/internal/go_templates/texttemplate/testdata/tmpl2.tmpl
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
template2
|
||||
{{define "y"}}y{{end}}
|
||||
{{template "x"}}
|
|
@ -24,7 +24,8 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
texttemplate "text/template"
|
||||
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ package safe
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
_strings "strings"
|
||||
"unicode/utf8"
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ package strings
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"errors"
|
||||
"html"
|
||||
"html/template"
|
||||
|
||||
"regexp"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
|
|
@ -15,6 +15,7 @@ package strings
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
|
@ -15,6 +15,8 @@ package tpl
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
@ -29,9 +31,8 @@ import (
|
|||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"html/template"
|
||||
texttemplate "text/template"
|
||||
"text/template/parse"
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
"github.com/gohugoio/hugo/metrics"
|
||||
|
|
|
@ -27,5 +27,4 @@ func TestExtractBaseof(t *testing.T) {
|
|||
c.Assert(replaced, qt.Equals, "_default/baseof.html")
|
||||
c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
|
||||
c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
|
||||
c.Assert(extractBaseOf("template: blog/baseof.ace:23:11:"), qt.Equals, "blog/baseof.ace")
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tplimpl
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/yosssi/ace"
|
||||
)
|
||||
|
||||
func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error {
|
||||
helpers.Deprecated("Ace", "See https://github.com/gohugoio/hugo/issues/6609", false)
|
||||
t.checkState()
|
||||
var base, inner *ace.File
|
||||
withoutExt := name[:len(name)-len(filepath.Ext(innerPath))]
|
||||
name = withoutExt + ".html"
|
||||
|
||||
// Fixes issue #1178
|
||||
basePath = strings.Replace(basePath, "\\", "/", -1)
|
||||
innerPath = strings.Replace(innerPath, "\\", "/", -1)
|
||||
|
||||
if basePath != "" {
|
||||
base = ace.NewFile(basePath, baseContent)
|
||||
inner = ace.NewFile(innerPath, innerContent)
|
||||
} else {
|
||||
base = ace.NewFile(innerPath, innerContent)
|
||||
inner = ace.NewFile("", []byte{})
|
||||
}
|
||||
|
||||
parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
|
||||
templ, err := ace.CompileResultWithTemplate(t.html.t.New(name), parsed, nil)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
|
||||
typ := resolveTemplateType(name)
|
||||
|
||||
c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if typ == templateShortcode {
|
||||
t.addShortcodeVariant(name, c.Info, templ)
|
||||
} else {
|
||||
t.templateInfo[name] = c.Info
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tplimpl
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/eknkc/amber"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) {
|
||||
helpers.Deprecated("Amber", "See https://github.com/gohugoio/hugo/issues/6609", false)
|
||||
c := amber.New()
|
||||
c.Options.VirtualFilesystem = afero.NewHttpFs(t.layoutsFs)
|
||||
|
||||
if err := c.ParseData(b, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := c.CompileString()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpl, err := templ.Funcs(t.amberFuncMap).Parse(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tpl, nil
|
||||
}
|
|
@ -15,17 +15,18 @@ package tplimpl
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"strings"
|
||||
texttemplate "text/template"
|
||||
"text/template/parse"
|
||||
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl/embedded"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/eknkc/amber"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/gohugoio/hugo/output"
|
||||
|
@ -56,9 +57,6 @@ var (
|
|||
_ templateFuncsterTemplater = (*textTemplates)(nil)
|
||||
)
|
||||
|
||||
// Protecting global map access (Amber)
|
||||
var amberMu sync.Mutex
|
||||
|
||||
type templateErr struct {
|
||||
name string
|
||||
err error
|
||||
|
@ -98,8 +96,6 @@ type templateHandler struct {
|
|||
text *textTemplates
|
||||
html *htmlTemplates
|
||||
|
||||
amberFuncMap template.FuncMap
|
||||
|
||||
errors []*templateErr
|
||||
|
||||
// This is the filesystem to load the templates from. All the templates are
|
||||
|
@ -778,23 +774,6 @@ func (t *templateHandler) initFuncs() {
|
|||
}
|
||||
}
|
||||
|
||||
// Amber is HTML only.
|
||||
t.amberFuncMap = template.FuncMap{}
|
||||
|
||||
amberMu.Lock()
|
||||
for k, v := range amber.FuncMap {
|
||||
t.amberFuncMap[k] = v
|
||||
}
|
||||
|
||||
for k, v := range t.html.funcster.funcMap {
|
||||
t.amberFuncMap[k] = v
|
||||
// Hacky, but we need to make sure that the func names are in the global map.
|
||||
amber.FuncMap[k] = func() string {
|
||||
panic("should never be invoked")
|
||||
}
|
||||
}
|
||||
amberMu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
func (t *templateHandler) getTemplateHandler(name string) templateLoader {
|
||||
|
@ -933,54 +912,11 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
|
|||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".amber":
|
||||
// Only HTML support for Amber
|
||||
withoutExt := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
templateName := withoutExt + ".html"
|
||||
b, err := afero.ReadFile(t.Layouts.Fs, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amberMu.Lock()
|
||||
templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName))
|
||||
amberMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ := resolveTemplateType(name)
|
||||
|
||||
c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if typ == templateShortcode {
|
||||
t.addShortcodeVariant(templateName, c.Info, templ)
|
||||
} else {
|
||||
t.templateInfo[name] = c.Info
|
||||
}
|
||||
|
||||
helpers.Deprecated("Amber templates are no longer supported.", "Use Go templates or a Hugo version <= 0.60.", true)
|
||||
return nil
|
||||
|
||||
case ".ace":
|
||||
// Only HTML support for Ace
|
||||
var innerContent, baseContent []byte
|
||||
innerContent, err := afero.ReadFile(t.Layouts.Fs, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if baseTemplatePath != "" {
|
||||
baseContent, err = afero.ReadFile(t.Layouts.Fs, baseTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
|
||||
helpers.Deprecated("ACE templates are no longer supported.", "Use Go templates or a Hugo version <= 0.60.", true)
|
||||
return nil
|
||||
default:
|
||||
|
||||
if baseTemplatePath != "" {
|
||||
|
|
|
@ -14,10 +14,12 @@
|
|||
package tplimpl
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
|
||||
"strings"
|
||||
texttemplate "text/template"
|
||||
"text/template/parse"
|
||||
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
|
|
@ -15,7 +15,8 @@ package tplimpl
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ package transform
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"html/template"
|
||||
|
||||
"net/url"
|
||||
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
|
|
Loading…
Add table
Reference in a new issue