diff --git a/.circleci/config.yml b/.circleci/config.yml
index fedba9ea4..ed9f399d4 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- - image: bepsays/ci-hugoreleaser:1.22600.20500
+ - image: bepsays/ci-hugoreleaser:1.22700.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- - image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20500
+ - image: bepsays/ci-hugoreleaser-linux-arm64:1.22700.20000
steps:
- *restore-cache
- &attach-workspace
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1c104cf5e..8646415e8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
- go-version: [1.26.x]
+ go-version: [1.27.x]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
diff --git a/Dockerfile b/Dockerfile
index ee9f9cc55..dc8926f26 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,8 +2,8 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
-ARG GO_VERSION="1.26"
-ARG ALPINE_VERSION="3.22"
+ARG GO_VERSION="1.27"
+ARG ALPINE_VERSION="3.24"
ARG DART_SASS_VERSION="1.79.3"
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
diff --git a/common/herrors/errors.go b/common/herrors/errors.go
index 5e223f42f..551e74401 100644
--- a/common/herrors/errors.go
+++ b/common/herrors/errors.go
@@ -178,3 +178,14 @@ func improveIfNilPointerMsg(inErr error) string {
s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s)
}
+
+// Or returns the first non-nil error from the given list of errors.
+// If all errors are nil, it returns nil.
+func Or(errs ...error) error {
+ for _, err := range errs {
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/common/hugio/copy.go b/common/hugio/copy.go
index 31d679dfc..18c2000d3 100644
--- a/common/hugio/copy.go
+++ b/common/hugio/copy.go
@@ -82,6 +82,9 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool
return err
}
} else {
+ if shouldCopy != nil && !shouldCopy(fromFilename) {
+ continue
+ }
if err := CopyFile(fs, fromFilename, toFilename); err != nil {
return err
}
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index 5613b177c..1a941ba1b 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -1017,15 +1017,20 @@ func newDefaultConfig() *Config {
Timeout: "60s",
CommonDirs: config.CommonDirs{
+ //lint:ignore SA1019 Keep as adapter for now.
ArcheTypeDir: "archetypes",
ContentDir: "content",
ResourceDir: "resources",
PublishDir: "public",
ThemesDir: "themes",
- AssetDir: "assets",
- LayoutDir: "layouts",
- I18nDir: "i18n",
- DataDir: "data",
+ //lint:ignore SA1019 Keep as adapter for now.
+ AssetDir: "assets",
+ //lint:ignore SA1019 Keep as adapter for now.
+ LayoutDir: "layouts",
+ //lint:ignore SA1019 Keep as adapter for now.
+ I18nDir: "i18n",
+ //lint:ignore SA1019 Keep as adapter for now.
+ DataDir: "data",
},
},
}
diff --git a/go.mod b/go.mod
index e10b249d2..db3f24bcf 100644
--- a/go.mod
+++ b/go.mod
@@ -186,4 +186,4 @@ require (
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)
-go 1.26.0
+go 1.27.0
diff --git a/hugolib/content_map_page_assembler.go b/hugolib/content_map_page_assembler.go
index 54cff0fc3..8fb0fbd34 100644
--- a/hugolib/content_map_page_assembler.go
+++ b/hugolib/content_map_page_assembler.go
@@ -14,7 +14,6 @@
package hugolib
import (
- "cmp"
"context"
"fmt"
"path"
@@ -23,6 +22,7 @@ import (
"github.com/bep/helpers/maphelpers"
"github.com/gohugoio/go-radix"
+ "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
@@ -134,7 +134,7 @@ func (a *allPagesAssembler) createAllPages() error {
}()
}
- if err := cmp.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
+ if err := herrors.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
return err
}
if err := a.pwRoot.WalkContext.HandleEventsAndHooks(); err != nil {
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index 6898e825b..b7ecbe25a 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -1179,23 +1179,7 @@ func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filen
t.Helper()
filename = filepath.Clean(filename)
b, err := afero.ReadFile(fs, filename)
- if err != nil {
- // Print some debug info
- hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator)
- start := 0
- if hadSlash {
- start = 1
- }
- end := start + 1
-
- parts := strings.Split(filename, helpers.FilePathSeparator)
- if parts[start] == "work" {
- end++
- }
-
- s.Assert(err, qt.IsNil)
-
- }
+ s.Assert(err, qt.IsNil)
return string(b)
}
diff --git a/hugolib/sitesmatrix/vectorstores.go b/hugolib/sitesmatrix/vectorstores.go
index b865bea65..574afcdf3 100644
--- a/hugolib/sitesmatrix/vectorstores.go
+++ b/hugolib/sitesmatrix/vectorstores.go
@@ -14,7 +14,6 @@
package sitesmatrix
import (
- "cmp"
"fmt"
"iter"
"maps"
@@ -780,7 +779,7 @@ func (b *IntSetsBuilder) Build() *IntSets {
}
func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
- applyFilter := func(what string, values []string, matcher ConfiguredDimension) (*hmaps.OrderedIntSet, error) {
+ applyFilter := func(what string, values []string, matcher ConfiguredDimension) *hmaps.OrderedIntSet {
var result *hmaps.OrderedIntSet
if len(values) == 0 {
@@ -800,16 +799,16 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
}
}
- return result, nil
+ return result
}
filter, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(values, matcher.ResolveIndex, hglob.GetGlobDot)
if err != nil {
- return nil, fmt.Errorf("failed to create filter for %s: %w", what, err)
+ panic(fmt.Errorf("failed to create filter for %s: %w", what, err))
}
iter, err := matcher.IndexMatch(filter)
if err != nil {
- return nil, fmt.Errorf("failed to match %s %q: %w", what, values, err)
+ panic(fmt.Errorf("failed to match %s %q: %w", what, values, err))
}
for i := range iter {
if result == nil {
@@ -818,16 +817,12 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
result.Set(i)
}
- return result, nil
+ return result
}
- l, err1 := applyFilter("languages", cfg.Globs.Languages, b.cfg.ConfiguredLanguages)
- v, err2 := applyFilter("versions", cfg.Globs.Versions, b.cfg.ConfiguredVersions)
- r, err3 := applyFilter("roles", cfg.Globs.Roles, b.cfg.ConfiguredRoles)
-
- if err := cmp.Or(err1, err2, err3); err != nil {
- panic(fmt.Errorf("failed to apply filters: %w", err))
- }
+ l := applyFilter("languages", cfg.Globs.Languages, b.cfg.ConfiguredLanguages)
+ v := applyFilter("versions", cfg.Globs.Versions, b.cfg.ConfiguredVersions)
+ r := applyFilter("roles", cfg.Globs.Roles, b.cfg.ConfiguredRoles)
b.GlobFilterMisses = Bools{
len(cfg.Globs.Languages) > 0 && l == nil,
diff --git a/internal/warpc/warpc.go b/internal/warpc/warpc.go
index be8e48bb5..59318eb1d 100644
--- a/internal/warpc/warpc.go
+++ b/internal/warpc/warpc.go
@@ -582,6 +582,9 @@ func (p *dispatcherPool[Q, R]) Err() error {
}
}
+// Workaround for data race, see https://github.com/wazero/wazero/issues/2532
+var wazeroCacheMu sync.Mutex
+
func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
if opts.Ctx == nil {
opts.Ctx = context.Background()
@@ -610,6 +613,8 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
runtimeConfig = runtimeConfig.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesThreads)
if opts.CompilationCacheDir != "" {
+ wazeroCacheMu.Lock()
+ defer wazeroCacheMu.Unlock()
compilationCache, err := wazero.NewCompilationCacheWithDir(opts.CompilationCacheDir)
if err != nil {
return nil, err
@@ -766,7 +771,9 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
}
for _, d := range dp.dispatchers {
- if err := d.inGroup.Wait(); err != nil {
+ // ErrShutdown is expected here; since Go 1.27 (json/v2), Decode
+ // surfaces the read error from the pipes we just closed.
+ if err := d.inGroup.Wait(); err != nil && err != ErrShutdown {
return err
}
}
diff --git a/resources/images/testdata/images_golden/filters/mask/transparant.png b/resources/images/testdata/images_golden/filters/mask/transparant.png
index c8f45165e..5d103ea5f 100644
Binary files a/resources/images/testdata/images_golden/filters/mask/transparant.png and b/resources/images/testdata/images_golden/filters/mask/transparant.png differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png
index 44af25303..1c1c980f2 100644
Binary files a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png
index 9bca47b14..5f18b8f17 100644
Binary files a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png differ
diff --git a/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png b/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png
index ec5042e67..b821ece6e 100644
Binary files a/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png and b/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png differ
diff --git a/resources/images/testdata/images_golden/process/webp/png.png b/resources/images/testdata/images_golden/process/webp/png.png
index b1f5eb89b..04d591b08 100644
Binary files a/resources/images/testdata/images_golden/process/webp/png.png and b/resources/images/testdata/images_golden/process/webp/png.png differ
diff --git a/resources/post_publish.go b/resources/post_publish.go
index 848755782..c8e59a612 100644
--- a/resources/post_publish.go
+++ b/resources/post_publish.go
@@ -26,10 +26,6 @@ type transformationKeyer interface {
func (spec *Spec) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) {
key := r.(transformationKeyer).TransformationKey()
return spec.PostProcessResources.GetOrCreate(key, func() (postpub.PostPublishedResource, error) {
- result := postpub.NewPostPublishResource(spec.incr.Incr(), r)
- if result == nil {
- panic("got nil result")
- }
- return result, nil
+ return postpub.NewPostPublishResource(spec.incr.Incr(), r), nil
})
}
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index b1af8621a..68890636d 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -15,8 +15,17 @@ import (
)
func main() {
- // The current is built with 2dc996f71b0ebafb77e64433e58333e049488a3c go1.26.3
- // TODO(bep) preserve the staticcheck.conf file.
+ /*
+ Previously: with 2dc996f71b0ebafb77e64433e58333e049488a3c go1.26.3
+ Current: 8af21751f0 [release-branch.go1.27] go1.27.0
+
+ Note that the upgrade here is mostly automatic, but:
+
+ * testenv.go is a stubbed Hugo version and is never overwritten; if the template tests start using new helpers from it, stub them in by hand.
+ * Some of the replacements below match exact upstream source; if upstream drifts, the build breaks and they need updating.
+ * Some test code depends on Go internals we don't fork; remove or stub these out to make it build.
+ * We're only patching the execution part of the template packages, so it's also good to check the execution package's Git history to check for valuable changes in our patched files.
+ */
fmt.Println("Forking ...")
defer fmt.Println("Done ...")
@@ -25,7 +34,7 @@ func main() {
htmlRoot := filepath.Join(forkRoot, "htmltemplate")
for _, pkg := range goPackages {
- copyGoPackage(pkg.dstPkg, pkg.srcPkg)
+ copyGoPackage(pkg.dstPkg, pkg.srcPkg, pkg.skip)
}
for _, pkg := range goPackages {
@@ -37,7 +46,6 @@ func main() {
}
const (
- // TODO(bep)
goSource = "/Users/bep/dev/go/misc/go/src"
forkRoot = "../../tpl/internal/go_templates"
)
@@ -47,6 +55,7 @@ type goPackage struct {
dstPkg string
replacer func(name, content string) string
rewriter func(name string)
+ skip func(name string) bool
}
var (
@@ -55,12 +64,14 @@ var (
`"internal/fmtsort"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`,
`"internal/testenv"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`,
"TestLinkerGC", "_TestLinkerGC",
+ "{new(int), true},", "//{new(int), true}, // Commented out for Hugo. We have a slightly different view on ... the truth.",
// Rename types and function that we want to overload.
"type state struct", "type stateOld struct",
"func (s *state) evalFunction", "func (s *state) evalFunctionOld",
"func (s *state) evalField(", "func (s *state) evalFieldOld(",
"func (s *state) evalCall(", "func (s *state) evalCallOld(",
"func (s *state) walkTemplate(", "func (s *state) walkTemplateOld(",
+ "func (s *state) validateType(", "func (s *state) _validateType(",
"func isTrue(val reflect.Value) (truth, ok bool) {", "func isTrueOld(val reflect.Value) (truth, ok bool) {",
)
@@ -74,26 +85,26 @@ var (
"\"text/template\"\n", "template \"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate\"\n",
`"html/template"`, `htmltemplate "html/template"`,
`"fmt"`, `htmltemplate "html/template"`,
+ `"internal/testenv"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`,
+ // Renamed so hugo_template.go can wrap it.
+ "func indirect(", "func doIndirect(",
`t.Skip("this test currently fails with -race; see issue #39807")`, `// t.Skip("this test currently fails with -race; see issue #39807")`,
)
+
+ // We don't want the internal/godebug dependency; meta content URL escaping is always on.
+ escapeGodebugReplacers = strings.NewReplacer(
+ `var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")`, ``,
+ `var htmlmetacontenturlescape = godebug.New("htmlmetacontenturlescape")`, `var htmlmetacontenturlescape = true`,
+ `if htmlmetacontenturlescape.Value() != "0" {`, `if htmlmetacontenturlescape {`,
+ )
)
func commonReplace(name, content string) string {
if strings.HasSuffix(name, "_test.go") {
- content = strings.Replace(content, "package template\n", `// +build go1.13,!windows
+ content = strings.Replace(content, "package template\n", `//go:build !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
@@ -111,6 +122,17 @@ var goPackages = []goPackage{
content = removeAll(`(?s)// Strings of content.*?\)\n`, content)
}
+ if strings.HasSuffix(name, "escape.go") {
+ content = escapeGodebugReplacers.Replace(content)
+ // Drop the else branch calling IncNonDefault; goimports removes the then-unused godebug import.
+ content = removeAll(` else \{\n(?:\t*//.*\n)*\t*htmlmetacontenturlescape\.IncNonDefault\(\)\n\t*\}`, content)
+ }
+
+ if strings.HasSuffix(name, "escape_test.go") {
+ // Tests the GODEBUG=htmlmetacontenturlescape=0 path, which we hard code to on.
+ content = removeAll(`(?s)func TestMetaContentEscapeGODEBUG.*?\n\}\n`, content)
+ }
+
content = commonReplace(name, content)
return htmlTemplateReplacers.Replace(content)
@@ -130,6 +152,11 @@ var goPackages = []goPackage{
replacer: func(name, content string) string { return testEnvReplacers.Replace(content) }, rewriter: func(name string) {
rewrite(name, `"internal/testenv" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`)
},
+ // testenv.go is a heavily stubbed Hugo version; keep it. The tests test the stubbed away parts.
+ skip: func(name string) bool {
+ base := filepath.Base(name)
+ return base == "testenv.go" || base == "testenv_test.go"
+ },
},
{srcPkg: "internal/cfg", dstPkg: "cfg", rewriter: func(name string) {
rewrite(name, `"internal/cfg" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"`)
@@ -140,10 +167,18 @@ var fs = afero.NewOsFs()
// Removes all non-Hugo files in the go_templates folder.
func cleanFork() {
+ keepRe := regexp.MustCompile(`(?i)hugo|staticcheck\.conf|^testenv\.go$`)
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))
+ if info.IsDir() || len(path) <= 10 {
+ return nil
}
+
+ if keepRe.MatchString(info.Name()) {
+ return nil
+ }
+
+ must(fs.Remove(path))
+
return nil
}))
}
@@ -154,11 +189,11 @@ func must(err error, what ...string) {
}
}
-func copyGoPackage(dst, src string) {
+func copyGoPackage(dst, src string, skip func(name string) bool) {
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 }))
+ must(hugio.CopyDir(fs, from, to, func(s string) bool { return skip == nil || !skip(s) }))
}
func doWithGoFiles(dir string,
diff --git a/tpl/internal/go_templates/htmltemplate/attr_string.go b/tpl/internal/go_templates/htmltemplate/attr_string.go
index 7159fa9cb..009458f42 100644
--- a/tpl/internal/go_templates/htmltemplate/attr_string.go
+++ b/tpl/internal/go_templates/htmltemplate/attr_string.go
@@ -22,8 +22,9 @@ const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcsetat
var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58, 73}
func (i attr) String() string {
- if i >= attr(len(_attr_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_attr_index)-1 {
return "attr(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _attr_name[_attr_index[i]:_attr_index[i+1]]
+ return _attr_name[_attr_index[idx]:_attr_index[idx+1]]
}
diff --git a/tpl/internal/go_templates/htmltemplate/clone_test.go b/tpl/internal/go_templates/htmltemplate/clone_test.go
index e42177a6b..a3d61b3ab 100644
--- a/tpl/internal/go_templates/htmltemplate/clone_test.go
+++ b/tpl/internal/go_templates/htmltemplate/clone_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
diff --git a/tpl/internal/go_templates/htmltemplate/content_test.go b/tpl/internal/go_templates/htmltemplate/content_test.go
index e886ee8ff..60648ec30 100644
--- a/tpl/internal/go_templates/htmltemplate/content_test.go
+++ b/tpl/internal/go_templates/htmltemplate/content_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
@@ -428,7 +427,7 @@ func TestStringer(t *testing.T) {
if err := tmpl.Execute(b, s); err != nil {
t.Fatal(err)
}
- expect := "string=3"
+ var expect = "string=3"
if b.String() != expect {
t.Errorf("expected %q got %q", expect, b.String())
}
diff --git a/tpl/internal/go_templates/htmltemplate/css_test.go b/tpl/internal/go_templates/htmltemplate/css_test.go
index f2b2add3a..6626bdda5 100644
--- a/tpl/internal/go_templates/htmltemplate/css_test.go
+++ b/tpl/internal/go_templates/htmltemplate/css_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
diff --git a/tpl/internal/go_templates/htmltemplate/delim_string.go b/tpl/internal/go_templates/htmltemplate/delim_string.go
index 8d8285022..eb0c3922e 100644
--- a/tpl/internal/go_templates/htmltemplate/delim_string.go
+++ b/tpl/internal/go_templates/htmltemplate/delim_string.go
@@ -19,8 +19,9 @@ 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) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_delim_index)-1 {
return "delim(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _delim_name[_delim_index[i]:_delim_index[i+1]]
+ return _delim_name[_delim_index[idx]:_delim_index[idx+1]]
}
diff --git a/tpl/internal/go_templates/htmltemplate/element_string.go b/tpl/internal/go_templates/htmltemplate/element_string.go
index bdf9da7b9..3ccaebcdf 100644
--- a/tpl/internal/go_templates/htmltemplate/element_string.go
+++ b/tpl/internal/go_templates/htmltemplate/element_string.go
@@ -21,8 +21,9 @@ const _element_name = "elementNoneelementScriptelementStyleelementTextareaelemen
var _element_index = [...]uint8{0, 11, 24, 36, 51, 63, 74}
func (i element) String() string {
- if i >= element(len(_element_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_element_index)-1 {
return "element(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _element_name[_element_index[i]:_element_index[i+1]]
+ return _element_name[_element_index[idx]:_element_index[idx+1]]
}
diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go
index a0b6dd79a..5710fa7b5 100644
--- a/tpl/internal/go_templates/htmltemplate/escape.go
+++ b/tpl/internal/go_templates/htmltemplate/escape.go
@@ -8,7 +8,6 @@ import (
"bytes"
"fmt"
"html"
- //"internal/godebug"
"io"
"maps"
"regexp"
@@ -165,9 +164,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
panic("escaping " + n.String() + " is unimplemented")
}
-//var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
-
-var htmlmetacontenturlescape = true //godebug.New("htmlmetacontenturlescape")
+var htmlmetacontenturlescape = true
// escapeAction escapes an action template node.
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go
index de75a6981..7759d060f 100644
--- a/tpl/internal/go_templates/htmltemplate/escape_test.go
+++ b/tpl/internal/go_templates/htmltemplate/escape_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
@@ -1866,7 +1865,7 @@ func TestEscapeText(t *testing.T) {
},
{
"`,
+ input: "a.b",
+ want: ``,
+ },
+ {
+ name: "regexp after close brace",
+ tmpl: ``,
+ input: "a.b",
+ want: ``,
+ },
+ {
+ name: "regexp pathological attacker input",
+ tmpl: ``,
+ input: `./;alert(1);var q=/.`,
+ want: ``,
+ },
+ {
+ name: "regexp after open brace in template literal",
+ tmpl: "",
+ input: "a.b",
+ want: "",
+ },
+ {
+ name: "regexp after close brace in template literal",
+ tmpl: "",
+ input: "a.b",
+ want: "",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpl := Must(New("test").Parse(tt.tmpl))
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, tt.input); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ if got := buf.String(); got != tt.want {
+ t.Errorf("got: %s\nwant: %s", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/tpl/internal/go_templates/htmltemplate/example_test.go b/tpl/internal/go_templates/htmltemplate/example_test.go
index 6485c7cfb..1fb06c633 100644
--- a/tpl/internal/go_templates/htmltemplate/example_test.go
+++ b/tpl/internal/go_templates/htmltemplate/example_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
diff --git a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go
index 43cc3bf01..24b22d984 100644
--- a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go
+++ b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go
index e01813e68..4f6c4d1c1 100644
--- a/tpl/internal/go_templates/htmltemplate/exec_test.go
+++ b/tpl/internal/go_templates/htmltemplate/exec_test.go
@@ -5,7 +5,6 @@
// Tests for template execution, copied from text/template.
//go:build !windows
-// +build !windows
package template
@@ -325,16 +324,12 @@ var execTests = []execTest{
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
- {
- "nested assignment",
+ {"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
- "3", tVal, true,
- },
- {
- "nested assignment changes the last declaration",
+ "3", tVal, true},
+ {"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
- "1", tVal, true,
- },
+ "1", tVal, true},
// Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template
@@ -381,21 +376,15 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
- {
- "method on chained var",
+ {"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
- "true", tVal, true,
- },
- {
- "chained method",
+ "true", tVal, true},
+ {"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
- "true", tVal, true,
- },
- {
- "chained method on variable",
+ "true", tVal, true},
+ {"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
- "true", tVal, true,
- },
+ "true", tVal, true},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -481,14 +470,10 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML.
- {
- "html", `{{html ""}}`,
- "<script>alert("XSS");</script>", nil, true,
- },
- {
- "html pipeline", `{{printf "" | html}}`,
- "<script>alert("XSS");</script>", nil, true,
- },
+ {"html", `{{html ""}}`,
+ "<script>alert("XSS");</script>", nil, true},
+ {"html pipeline", `{{printf "" | html}}`,
+ "<script>alert("XSS");</script>", nil, true},
{"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" in text/template
@@ -854,7 +839,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) {
const hello = "Hello, world"
- value := struct{ Str string }{hello}
+ var value = struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 {
text := ".Str"
left := delimPairs[i+0]
@@ -877,7 +862,7 @@ func TestDelims(t *testing.T) {
if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err)
}
- b := new(strings.Builder)
+ var b = new(strings.Builder)
err = tmpl.Execute(b, value)
if err != nil {
t.Fatalf("delim %q exec err %s", left, err)
@@ -978,7 +963,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
- tree := &Tree{
+ var tree = &Tree{
1,
&Tree{
2, &Tree{
@@ -1229,7 +1214,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) {
b := new(strings.Builder)
- cmpStruct := struct {
+ var cmpStruct = struct {
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
diff --git a/tpl/internal/go_templates/htmltemplate/html_test.go b/tpl/internal/go_templates/htmltemplate/html_test.go
index bea668ef9..5983c592b 100644
--- a/tpl/internal/go_templates/htmltemplate/html_test.go
+++ b/tpl/internal/go_templates/htmltemplate/html_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go
index 22ebdcfed..c4f89e5dd 100644
--- a/tpl/internal/go_templates/htmltemplate/js_test.go
+++ b/tpl/internal/go_templates/htmltemplate/js_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
@@ -221,8 +220,7 @@ func TestJSStrEscaper(t *testing.T) {
{"", `--\u003e`},
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
- {
- "+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
+ {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
`\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
},
// Invalid UTF-8 sequence
diff --git a/tpl/internal/go_templates/htmltemplate/jsctx_string.go b/tpl/internal/go_templates/htmltemplate/jsctx_string.go
index 23948934c..103d5559f 100644
--- a/tpl/internal/go_templates/htmltemplate/jsctx_string.go
+++ b/tpl/internal/go_templates/htmltemplate/jsctx_string.go
@@ -18,8 +18,9 @@ const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
var _jsCtx_index = [...]uint8{0, 11, 21, 33}
func (i jsCtx) String() string {
- if i >= jsCtx(len(_jsCtx_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_jsCtx_index)-1 {
return "jsCtx(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _jsCtx_name[_jsCtx_index[i]:_jsCtx_index[i+1]]
+ return _jsCtx_name[_jsCtx_index[idx]:_jsCtx_index[idx+1]]
}
diff --git a/tpl/internal/go_templates/htmltemplate/multi_test.go b/tpl/internal/go_templates/htmltemplate/multi_test.go
index c320c4353..9107b3611 100644
--- a/tpl/internal/go_templates/htmltemplate/multi_test.go
+++ b/tpl/internal/go_templates/htmltemplate/multi_test.go
@@ -5,7 +5,6 @@
// Tests for multiple-template execution, copied from text/template.
//go:build !windows
-// +build !windows
package template
@@ -268,7 +267,7 @@ func TestIssue19294(t *testing.T) {
// 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.
- inlined := map[string]string{
+ var inlined = map[string]string{
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
"xhtml": `{{block "stylesheet" .}}{{end}}`,
}
diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go
index f5a70b223..407064e8d 100644
--- a/tpl/internal/go_templates/htmltemplate/state_string.go
+++ b/tpl/internal/go_templates/htmltemplate/state_string.go
@@ -46,8 +46,9 @@ const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValu
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 363, 382, 391}
func (i state) String() string {
- if i >= state(len(_state_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_state_index)-1 {
return "state(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _state_name[_state_index[i]:_state_index[i+1]]
+ return _state_name[_state_index[idx]:_state_index[idx+1]]
}
diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go
index 4582ddd5f..f6be5d4c2 100644
--- a/tpl/internal/go_templates/htmltemplate/template.go
+++ b/tpl/internal/go_templates/htmltemplate/template.go
@@ -26,7 +26,8 @@ type Template struct {
// 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.
+ // The underlying template's parse tree, updated to be HTML-safe
+ // after the first execution.
Tree *parse.Tree
*nameSpace // common to all associated templates
}
@@ -332,10 +333,12 @@ func (t *Template) Name() string {
type FuncMap = template.FuncMap
// Funcs adds the elements of the argument map to the template's function map.
-// It must be called before the template is parsed.
+// Any function used in the template must be added before the template is
+// parsed. Funcs may be called more than once, including after parsing (for
+// example, after [Template.Clone]), to replace a function of the same name;
+// the replacement is used when the template is executed.
// 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.
+// type. 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
diff --git a/tpl/internal/go_templates/htmltemplate/template_test.go b/tpl/internal/go_templates/htmltemplate/template_test.go
index c5a5df437..b348825de 100644
--- a/tpl/internal/go_templates/htmltemplate/template_test.go
+++ b/tpl/internal/go_templates/htmltemplate/template_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go
index ea4b272cc..d9d4f63be 100644
--- a/tpl/internal/go_templates/htmltemplate/transition.go
+++ b/tpl/internal/go_templates/htmltemplate/transition.go
@@ -336,11 +336,14 @@ func tJS(c context, s []byte) (context, int) {
// We only care about tracking brace depth if we are inside of a
// template literal.
if len(c.jsBraceDepth) == 0 {
+ c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
return c, i + 1
}
c.jsBraceDepth[len(c.jsBraceDepth)-1]++
+ c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
case '}':
if len(c.jsBraceDepth) == 0 {
+ c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
return c, i + 1
}
// There are no cases where a brace can be escaped in the JS context
@@ -349,6 +352,7 @@ func tJS(c context, s []byte) (context, int) {
// fully fledged parsers will just fail anyway.
c.jsBraceDepth[len(c.jsBraceDepth)-1]--
if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
+ c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
return c, i + 1
}
c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
@@ -426,7 +430,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
// If " 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte(" 0 && i+7 <= len(s) && bytes.EqualFold(s[i-1:i+7], []byte("= urlPart(len(_urlPart_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_urlPart_index)-1 {
return "urlPart(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _urlPart_name[_urlPart_index[i]:_urlPart_index[i+1]]
+ return _urlPart_name[_urlPart_index[idx]:_urlPart_index[idx+1]]
}
diff --git a/tpl/internal/go_templates/testenv/testenv_unix.go b/tpl/internal/go_templates/testenv/testenv_unix.go
index e325050a9..22eeca220 100644
--- a/tpl/internal/go_templates/testenv/testenv_unix.go
+++ b/tpl/internal/go_templates/testenv/testenv_unix.go
@@ -7,6 +7,8 @@
package testenv
import (
+ "errors"
+ "io/fs"
"syscall"
)
@@ -19,5 +21,22 @@ func syscallIsNotSupported(err error) bool {
return false
}
+ if errno, ok := errors.AsType[syscall.Errno](err); ok {
+ switch errno {
+ case syscall.EPERM, syscall.EROFS:
+ // User lacks permission: either the call requires root permission and the
+ // user is not root, or the call is denied by a container security policy.
+ return true
+ case syscall.EINVAL:
+ // Some containers return EINVAL instead of EPERM if a system call is
+ // denied by security policy.
+ return true
+ }
+ }
+
+ if errors.Is(err, fs.ErrPermission) || errors.Is(err, errors.ErrUnsupported) {
+ return true
+ }
+
return false
}
diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go
index 295a810b8..9cab2e832 100644
--- a/tpl/internal/go_templates/texttemplate/example_test.go
+++ b/tpl/internal/go_templates/texttemplate/example_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
diff --git a/tpl/internal/go_templates/texttemplate/examplefiles_test.go b/tpl/internal/go_templates/texttemplate/examplefiles_test.go
index bc91e87f9..6534ee331 100644
--- a/tpl/internal/go_templates/texttemplate/examplefiles_test.go
+++ b/tpl/internal/go_templates/texttemplate/examplefiles_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
diff --git a/tpl/internal/go_templates/texttemplate/examplefunc_test.go b/tpl/internal/go_templates/texttemplate/examplefunc_test.go
index 0c745bdd4..ace615def 100644
--- a/tpl/internal/go_templates/texttemplate/examplefunc_test.go
+++ b/tpl/internal/go_templates/texttemplate/examplefunc_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
@@ -57,7 +54,7 @@ Output 2: {{printf "%q" . | title}}
}
// This example demonstrates registering two custom template functions
-// and how to overwite one of the functions after the template has been
+// and how to overwrite one of the functions after the template has been
// parsed. Overwriting can be used, for example, to alter the operation
// of cloned templates.
func ExampleTemplate_funcs() {
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index 6b677456d..f63856c23 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
@@ -337,16 +336,12 @@ var execTests = []execTest{
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
- {
- "nested assignment",
+ {"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
- "3", tVal, true,
- },
- {
- "nested assignment changes the last declaration",
+ "3", tVal, true},
+ {"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
- "1", tVal, true,
- },
+ "1", tVal, true},
// Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true},
@@ -393,21 +388,15 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: -", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: -", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
- {
- "method on chained var",
+ {"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
- "true", tVal, true,
- },
- {
- "chained method",
+ "true", tVal, true},
+ {"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
- "true", tVal, true,
- },
- {
- "chained method on variable",
+ "true", tVal, true},
+ {"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
- "true", tVal, true,
- },
+ "true", tVal, true},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -500,14 +489,10 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML.
- {
- "html", `{{html ""}}`,
- "<script>alert("XSS");</script>", nil, true,
- },
- {
- "html pipeline", `{{printf "" | html}}`,
- "<script>alert("XSS");</script>", nil, true,
- },
+ {"html", `{{html ""}}`,
+ "<script>alert("XSS");</script>", nil, true},
+ {"html pipeline", `{{printf "" | html}}`,
+ "<script>alert("XSS");</script>", nil, true},
{"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "<no value>", tVal, true},
@@ -942,9 +927,8 @@ var delimPairs = []string{
func TestDelims(t *testing.T) {
const hello = "Hello, world"
- value := struct{ Str string }{hello}
+ var value = struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 {
- text := ".Str"
left := delimPairs[i+0]
trueLeft := left
right := delimPairs[i+1]
@@ -955,17 +939,23 @@ func TestDelims(t *testing.T) {
if right == "" { // default case
trueRight = "}}"
}
- text = trueLeft + text + trueRight
- // Now add a comment
- text += trueLeft + "/*comment*/" + trueRight
- // Now add an action containing a string.
- text += trueLeft + `"` + trueLeft + `"` + trueRight
+ action := trueLeft + ".Str" + trueRight
+ // A comment, which is not preserved in the parse tree.
+ comment := trueLeft + "/*comment*/" + trueRight
+ // An action containing a string that looks like the left delimiter.
+ strAction := trueLeft + `"` + trueLeft + `"` + trueRight
+ text := action + comment + strAction
// At this point text looks like `{{.Str}}{{/*comment*/}}{{"{{"}}`.
tmpl, err := New("delims").Delims(left, right).Parse(text)
if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err)
}
- b := new(strings.Builder)
+ // The parse tree's String form should roundtrip back to the input,
+ // using the custom delimiters, modulo the dropped comment.
+ if got, want := tmpl.Root.String(), action+strAction; got != want {
+ t.Errorf("delim %q: String() = %q, want %q", left, got, want)
+ }
+ var b = new(strings.Builder)
err = tmpl.Execute(b, value)
if err != nil {
t.Fatalf("delim %q exec err %s", left, err)
@@ -1025,6 +1015,23 @@ type CustomError struct{}
func (*CustomError) Error() string { return "heyo !" }
+// Check that a custom error can be returned.
+func TestExecError_CustomError(t *testing.T) {
+ failingFunc := func() (string, error) {
+ return "", &CustomError{}
+ }
+ tmpl := Must(New("top").Funcs(FuncMap{
+ "err": failingFunc,
+ }).Parse("{{ err }}"))
+
+ var b bytes.Buffer
+ err := tmpl.Execute(&b, nil)
+
+ if _, ok := errors.AsType[*CustomError](err); !ok {
+ t.Fatalf("expected custom error; got %s", err)
+ }
+}
+
func TestJSEscaping(t *testing.T) {
testCases := []struct {
in, exp string
@@ -1070,7 +1077,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
- tree := &Tree{
+ var tree = &Tree{
1,
&Tree{
2, &Tree{
@@ -1323,7 +1330,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) {
b := new(strings.Builder)
- cmpStruct := struct {
+ var cmpStruct = struct {
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
@@ -1836,13 +1843,12 @@ func TestFunctionCheckDuringCall(t *testing.T) {
input string
data any
wantErr string
- }{
- {
- name: "call nothing",
- input: `{{call}}`,
- data: tVal,
- wantErr: "wrong number of args for call: want at least 1 got 0",
- },
+ }{{
+ name: "call nothing",
+ input: `{{call}}`,
+ data: tVal,
+ wantErr: "wrong number of args for call: want at least 1 got 0",
+ },
{
name: "call non-function",
input: "{{call .True}}",
diff --git a/tpl/internal/go_templates/texttemplate/link_test.go b/tpl/internal/go_templates/texttemplate/link_test.go
index 23f6a31fa..0dabb65f4 100644
--- a/tpl/internal/go_templates/texttemplate/link_test.go
+++ b/tpl/internal/go_templates/texttemplate/link_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package template_test
import (
diff --git a/tpl/internal/go_templates/texttemplate/multi_test.go b/tpl/internal/go_templates/texttemplate/multi_test.go
index 8fda90d74..b2c22c440 100644
--- a/tpl/internal/go_templates/texttemplate/multi_test.go
+++ b/tpl/internal/go_templates/texttemplate/multi_test.go
@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !windows
-// +build !windows
package template
@@ -11,11 +10,10 @@ package template
import (
"fmt"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"os"
"strings"
"testing"
-
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
const (
@@ -32,32 +30,22 @@ type multiParseTest struct {
}
var multiParseTests = []multiParseTest{
- {
- "empty", "", noError,
+ {"empty", "", noError,
nil,
- nil,
- },
- {
- "one", `{{define "foo"}} FOO {{end}}`, noError,
+ nil},
+ {"one", `{{define "foo"}} FOO {{end}}`, noError,
[]string{"foo"},
- []string{" FOO "},
- },
- {
- "two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
+ []string{" FOO "}},
+ {"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
[]string{"foo", "bar"},
- []string{" FOO ", " BAR "},
- },
+ []string{" FOO ", " BAR "}},
// errors
- {
- "missing end", `{{define "foo"}} FOO `, hasError,
+ {"missing end", `{{define "foo"}} FOO `, hasError,
nil,
+ nil},
+ {"malformed name", `{{define "foo}} FOO `, hasError,
nil,
- },
- {
- "malformed name", `{{define "foo}} FOO `, hasError,
- nil,
- nil,
- },
+ nil},
}
func TestMultiParse(t *testing.T) {
@@ -454,7 +442,7 @@ func TestIssue19294(t *testing.T) {
// 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.
- inlined := map[string]string{
+ var inlined = map[string]string{
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
"xhtml": `{{block "stylesheet" .}}{{end}}`,
}
diff --git a/tpl/internal/go_templates/texttemplate/parse/lex.go b/tpl/internal/go_templates/texttemplate/parse/lex.go
index a00f48e65..fa9eaf5a3 100644
--- a/tpl/internal/go_templates/texttemplate/parse/lex.go
+++ b/tpl/internal/go_templates/texttemplate/parse/lex.go
@@ -240,10 +240,10 @@ func (l *lexer) nextItem() item {
// lex creates a new scanner for the input string.
func lex(name, input, left, right string) *lexer {
if left == "" {
- left = leftDelim
+ left = defaultLeftDelim
}
if right == "" {
- right = rightDelim
+ right = defaultRightDelim
}
l := &lexer{
name: name,
@@ -260,10 +260,10 @@ func lex(name, input, left, right string) *lexer {
// state functions
const (
- leftDelim = "{{"
- rightDelim = "}}"
- leftComment = "/*"
- rightComment = "*/"
+ defaultLeftDelim = "{{"
+ defaultRightDelim = "}}"
+ leftComment = "/*"
+ rightComment = "*/"
)
// lexText scans until an opening action delimiter, "{{".
diff --git a/tpl/internal/go_templates/texttemplate/parse/lex_test.go b/tpl/internal/go_templates/texttemplate/parse/lex_test.go
index 633ffb055..bd535c4ce 100644
--- a/tpl/internal/go_templates/texttemplate/parse/lex_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/lex_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package parse
import (
diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go
index a31309874..31d663c8d 100644
--- a/tpl/internal/go_templates/texttemplate/parse/node.go
+++ b/tpl/internal/go_templates/texttemplate/parse/node.go
@@ -171,9 +171,9 @@ func (c *CommentNode) String() string {
}
func (c *CommentNode) writeTo(sb *strings.Builder) {
- sb.WriteString("{{")
+ sb.WriteString(c.tr.leftDelim)
sb.WriteString(c.Text)
- sb.WriteString("}}")
+ sb.WriteString(c.tr.rightDelim)
}
func (c *CommentNode) tree() *Tree {
@@ -277,9 +277,9 @@ func (a *ActionNode) String() string {
}
func (a *ActionNode) writeTo(sb *strings.Builder) {
- sb.WriteString("{{")
+ sb.WriteString(a.tr.leftDelim)
a.Pipe.writeTo(sb)
- sb.WriteString("}}")
+ sb.WriteString(a.tr.rightDelim)
}
func (a *ActionNode) tree() *Tree {
@@ -793,7 +793,7 @@ func (t *Tree) newEnd(pos Pos) *endNode {
}
func (e *endNode) String() string {
- return "{{end}}"
+ return e.tr.leftDelim + "end" + e.tr.rightDelim
}
func (e *endNode) writeTo(sb *strings.Builder) {
@@ -825,7 +825,7 @@ func (e *elseNode) Type() NodeType {
}
func (e *elseNode) String() string {
- return "{{else}}"
+ return e.tr.leftDelim + "else" + e.tr.rightDelim
}
func (e *elseNode) writeTo(sb *strings.Builder) {
@@ -869,17 +869,21 @@ func (b *BranchNode) writeTo(sb *strings.Builder) {
default:
panic("unknown branch type")
}
- sb.WriteString("{{")
+ sb.WriteString(b.tr.leftDelim)
sb.WriteString(name)
sb.WriteByte(' ')
b.Pipe.writeTo(sb)
- sb.WriteString("}}")
+ sb.WriteString(b.tr.rightDelim)
b.List.writeTo(sb)
if b.ElseList != nil {
- sb.WriteString("{{else}}")
+ sb.WriteString(b.tr.leftDelim)
+ sb.WriteString("else")
+ sb.WriteString(b.tr.rightDelim)
b.ElseList.writeTo(sb)
}
- sb.WriteString("{{end}}")
+ sb.WriteString(b.tr.leftDelim)
+ sb.WriteString("end")
+ sb.WriteString(b.tr.rightDelim)
}
func (b *BranchNode) tree() *Tree {
@@ -925,9 +929,9 @@ func (t *Tree) newBreak(pos Pos, line int) *BreakNode {
}
func (b *BreakNode) Copy() Node { return b.tr.newBreak(b.Pos, b.Line) }
-func (b *BreakNode) String() string { return "{{break}}" }
+func (b *BreakNode) String() string { return b.tr.leftDelim + "break" + b.tr.rightDelim }
func (b *BreakNode) tree() *Tree { return b.tr }
-func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") }
+func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString(b.String()) }
// ContinueNode represents a {{continue}} action.
type ContinueNode struct {
@@ -942,9 +946,9 @@ func (t *Tree) newContinue(pos Pos, line int) *ContinueNode {
}
func (c *ContinueNode) Copy() Node { return c.tr.newContinue(c.Pos, c.Line) }
-func (c *ContinueNode) String() string { return "{{continue}}" }
+func (c *ContinueNode) String() string { return c.tr.leftDelim + "continue" + c.tr.rightDelim }
func (c *ContinueNode) tree() *Tree { return c.tr }
-func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") }
+func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString(c.String()) }
// RangeNode represents a {{range}} action and its commands.
type RangeNode struct {
@@ -993,13 +997,14 @@ func (t *TemplateNode) String() string {
}
func (t *TemplateNode) writeTo(sb *strings.Builder) {
- sb.WriteString("{{template ")
+ sb.WriteString(t.tr.leftDelim)
+ sb.WriteString("template ")
sb.WriteString(strconv.Quote(t.Name))
if t.Pipe != nil {
sb.WriteByte(' ')
t.Pipe.writeTo(sb)
}
- sb.WriteString("}}")
+ sb.WriteString(t.tr.rightDelim)
}
func (t *TemplateNode) tree() *Tree {
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
index b74dfb7f4..ae81be249 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse.go
@@ -33,6 +33,9 @@ type Tree struct {
actionLine int // line of left delim starting action
rangeDepth int
stackDepth int // depth of nested parenthesized expressions
+
+ leftDelim string
+ rightDelim string
}
// A Mode value is a set of flags (or 0). Modes control parser behavior.
@@ -60,10 +63,12 @@ func (t *Tree) Copy() *Tree {
return nil
}
return &Tree{
- Name: t.Name,
- ParseName: t.ParseName,
- Root: t.Root.CopyList(),
- text: t.text,
+ Name: t.Name,
+ ParseName: t.ParseName,
+ Root: t.Root.CopyList(),
+ text: t.text,
+ leftDelim: t.leftDelim,
+ rightDelim: t.rightDelim,
}
}
@@ -258,7 +263,15 @@ func (t *Tree) stopParse() {
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) {
defer t.recover(&err)
t.ParseName = t.Name
- lexer := lex(t.Name, text, leftDelim, rightDelim)
+ t.leftDelim = leftDelim
+ if t.leftDelim == "" {
+ t.leftDelim = defaultLeftDelim
+ }
+ t.rightDelim = rightDelim
+ if t.rightDelim == "" {
+ t.rightDelim = defaultRightDelim
+ }
+ lexer := lex(t.Name, text, t.leftDelim, t.rightDelim)
t.startParse(funcs, lexer, treeSet)
t.text = text
t.parse()
@@ -318,6 +331,8 @@ func (t *Tree) parse() {
newT := New("definition") // name will be updated once we know it.
newT.text = t.text
newT.Mode = t.Mode
+ newT.leftDelim = t.leftDelim
+ newT.rightDelim = t.rightDelim
newT.ParseName = t.ParseName
newT.startParse(t.funcs, t.lex, t.treeSet)
newT.parseDefinition()
@@ -546,7 +561,7 @@ func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode,
t.rangeDepth--
}
switch next.Type() {
- case nodeEnd: //done
+ case nodeEnd: // done
case nodeElse:
// Special case for "else if" and "else with".
// If the "else" is followed immediately by an "if" or "with",
@@ -650,6 +665,8 @@ func (t *Tree) blockControl() Node {
block := New(name) // name will be updated once we know it.
block.text = t.text
block.Mode = t.Mode
+ block.leftDelim = t.leftDelim
+ block.rightDelim = t.rightDelim
block.ParseName = t.ParseName
block.startParse(t.funcs, t.lex, t.treeSet)
var end Node
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
index 90ec9852b..8269e6800 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13
-// +build go1.13
-
package parse
import (
@@ -415,6 +412,36 @@ func TestParseWithComments(t *testing.T) {
}
}
+func TestDelimsStringRoundtrip(t *testing.T) {
+ // Each input is already in canonical String form, so parsing it with the
+ // custom delimiters and printing the resulting tree must reproduce the
+ // input exactly. This exercises the String/writeTo methods of every node
+ // type that emits delimiters.
+ const (
+ left = "[["
+ right = "]]"
+ )
+ for _, input := range []string{
+ `[[.X]]`, // ActionNode
+ `[[/* a comment */]]`, // CommentNode
+ `[[if .X]]y[[else]]z[[end]]`, // BranchNode (if), with else and end
+ `[[range .X]][[break]][[continue]][[end]]`, // RangeNode, BreakNode, ContinueNode
+ `[[with .X]]y[[end]]`, // BranchNode (with)
+ `[[template "name" .]]`, // TemplateNode
+ } {
+ tr := New("test")
+ tr.Mode = ParseComments
+ tmpl, err := tr.Parse(input, left, right, make(map[string]*Tree))
+ if err != nil {
+ t.Errorf("%q: unexpected parse error: %v", input, err)
+ continue
+ }
+ if got := tmpl.Root.String(); got != input {
+ t.Errorf("got\n\t%q\nexpected\n\t%q", got, input)
+ }
+ }
+}
+
func TestKeywordsAndFuncs(t *testing.T) {
// Check collisions between functions and new keywords like 'break'. When a
// break function is provided, the parser should treat 'break' as a function,
diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go
index e66c01fd4..43f5cd416 100644
--- a/tpl/internal/go_templates/texttemplate/template.go
+++ b/tpl/internal/go_templates/texttemplate/template.go
@@ -167,11 +167,13 @@ func (t *Template) Delims(left, right string) *Template {
}
// Funcs adds the elements of the argument map to the template's function map.
-// It must be called before the template is parsed.
+// Any function used in the template must be added before the template is
+// parsed. Funcs may be called more than once, including after parsing (for
+// example, after [Template.Clone]), to replace a function of the same name;
+// the replacement is used when the template is executed.
// 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.
+// The return value is the template, so calls can be chained.
func (t *Template) Funcs(funcMap FuncMap) *Template {
t.init()
t.muFuncs.Lock()