aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-08-14 11:34:21 +0200
committerBjørn Erik Pedersen <[email protected]>2024-08-15 10:18:19 +0200
commit2168c5b125020a1841450730edc1b0ed2141d239 (patch)
tree7edfa01d9f1e0c921f08a468f9e5202e5569c529
parentb3ad58fa04fb2447d1a788d1fe61b2ed581a403e (diff)
downloadhugo-2168c5b125020a1841450730edc1b0ed2141d239.tar.gz
hugo-2168c5b125020a1841450730edc1b0ed2141d239.zip
Upgrade to Go 1.23
Fixes #12763
-rw-r--r--.circleci/config.yml4
-rw-r--r--.github/workflows/test.yml2
-rw-r--r--go.mod2
-rw-r--r--hugofs/fs.go3
-rw-r--r--modules/client.go12
-rw-r--r--scripts/fork_go_templates/main.go2
-rw-r--r--testscripts/commands/mod.txt3
-rw-r--r--testscripts/commands/mod_npm.txt2
-rw-r--r--testscripts/commands/mod_npm_withexisting.txt1
-rw-r--r--tpl/internal/go_templates/cfg/cfg.go2
-rw-r--r--tpl/internal/go_templates/fmtsort/sort.go113
-rw-r--r--tpl/internal/go_templates/fmtsort/sort_test.go17
-rw-r--r--tpl/internal/go_templates/htmltemplate/content.go5
-rw-r--r--tpl/internal/go_templates/htmltemplate/doc.go10
-rw-r--r--tpl/internal/go_templates/htmltemplate/examplefiles_test.go3
-rw-r--r--tpl/internal/go_templates/htmltemplate/exec_test.go6
-rw-r--r--tpl/internal/go_templates/htmltemplate/js.go5
-rw-r--r--tpl/internal/go_templates/htmltemplate/template.go22
-rw-r--r--tpl/internal/go_templates/htmltemplate/transition.go2
-rw-r--r--tpl/internal/go_templates/testenv/testenv.go21
-rw-r--r--tpl/internal/go_templates/texttemplate/doc.go7
-rw-r--r--tpl/internal/go_templates/texttemplate/example_test.go2
-rw-r--r--tpl/internal/go_templates/texttemplate/examplefiles_test.go3
-rw-r--r--tpl/internal/go_templates/texttemplate/exec.go39
-rw-r--r--tpl/internal/go_templates/texttemplate/exec_test.go102
-rw-r--r--tpl/internal/go_templates/texttemplate/funcs.go46
-rw-r--r--tpl/internal/go_templates/texttemplate/helper.go34
-rw-r--r--tpl/internal/go_templates/texttemplate/hugo_template.go13
-rw-r--r--tpl/internal/go_templates/texttemplate/link_test.go8
-rw-r--r--tpl/internal/go_templates/texttemplate/parse/node.go12
-rw-r--r--tpl/internal/go_templates/texttemplate/parse/parse.go60
-rw-r--r--tpl/internal/go_templates/texttemplate/parse/parse_test.go435
-rw-r--r--tpl/internal/go_templates/texttemplate/template.go6
-rw-r--r--tpl/tplimpl/tplimpl_integration_test.go14
34 files changed, 616 insertions, 402 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index beee3b536..0ff955936 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.22200.20501
+ - image: bepsays/ci-hugoreleaser:1.22300.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -60,7 +60,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- - image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20501
+ - image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20000
steps:
- *restore-cache
- &attach-workspace
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b5491090e..120262098 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
- go-version: [1.21.x, 1.22.x]
+ go-version: [1.22.x, 1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
diff --git a/go.mod b/go.mod
index f9776279e..40f90c45f 100644
--- a/go.mod
+++ b/go.mod
@@ -170,4 +170,4 @@ require (
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
)
-go 1.21.8
+go 1.22.6
diff --git a/hugofs/fs.go b/hugofs/fs.go
index fc0ea71c6..fab0d3886 100644
--- a/hugofs/fs.go
+++ b/hugofs/fs.go
@@ -147,7 +147,8 @@ func isWrite(flag int) bool {
// TODO(bep) move this to a more suitable place.
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
// Safe guard
- if !strings.Contains(dir, "pkg") {
+ // Note that the base directory changed from pkg to gomod_cache in Go 1.23.
+ if !strings.Contains(dir, "pkg") && !strings.Contains(dir, "gomod") {
panic(fmt.Sprint("invalid dir:", dir))
}
diff --git a/modules/client.go b/modules/client.go
index a6caec23c..dce40d2db 100644
--- a/modules/client.go
+++ b/modules/client.go
@@ -365,18 +365,6 @@ func (c *Client) Get(args ...string) error {
}
func (c *Client) get(args ...string) error {
- var hasD bool
- for _, arg := range args {
- if arg == "-d" {
- hasD = true
- break
- }
- }
- if !hasD {
- // go get without the -d flag does not make sense to us, as
- // it will try to build and install go packages.
- args = append([]string{"-d"}, args...)
- }
if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
return fmt.Errorf("failed to get %q: %w", args, err)
}
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index 476fe480b..c4dad3224 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -16,7 +16,7 @@ import (
)
func main() {
- // The current is built with 8e1fdea8316d840fd07e9d6e026048e53290948b go1.22.5
+ // The current is built with 6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0
// TODO(bep) preserve the staticcheck.conf file.
fmt.Println("Forking ...")
defer fmt.Println("Done ...")
diff --git a/testscripts/commands/mod.txt b/testscripts/commands/mod.txt
index 56cea2c00..2fa17dbbe 100644
--- a/testscripts/commands/mod.txt
+++ b/testscripts/commands/mod.txt
@@ -18,7 +18,8 @@ hugo mod clean
! stderr .
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
hugo mod clean --all
-stdout 'Deleted 2\d{2} files from module cache\.'
+# Currently this is 299 on MacOS and 301 on Linux.
+stdout 'Deleted (2|3)\d{2} files from module cache\.'
cd submod
hugo mod init testsubmod
cmpenv go.mod $WORK/golden/go.mod.testsubmod
diff --git a/testscripts/commands/mod_npm.txt b/testscripts/commands/mod_npm.txt
index 32cc37f06..3d8903e6a 100644
--- a/testscripts/commands/mod_npm.txt
+++ b/testscripts/commands/mod_npm.txt
@@ -2,6 +2,7 @@
dostounix golden/package.json
+
hugo mod npm pack
cmp package.json golden/package.json
@@ -41,3 +42,4 @@ path="github.com/gohugoio/hugoTestModule2"
}
-- go.mod --
module github.com/gohugoio/hugoTestModule
+go 1.20
diff --git a/testscripts/commands/mod_npm_withexisting.txt b/testscripts/commands/mod_npm_withexisting.txt
index e92eba3fd..073af0f07 100644
--- a/testscripts/commands/mod_npm_withexisting.txt
+++ b/testscripts/commands/mod_npm_withexisting.txt
@@ -55,3 +55,4 @@ path="github.com/gohugoio/hugoTestModule2"
}
-- go.mod --
module github.com/gohugoio/hugoTestModule
+go 1.20
diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go
index 2af0ec707..08d210b79 100644
--- a/tpl/internal/go_templates/cfg/cfg.go
+++ b/tpl/internal/go_templates/cfg/cfg.go
@@ -36,6 +36,7 @@ const KnownEnv = `
GOAMD64
GOARCH
GOARM
+ GOARM64
GOBIN
GOCACHE
GOCACHEPROG
@@ -57,6 +58,7 @@ const KnownEnv = `
GOPPC64
GOPRIVATE
GOPROXY
+ GORISCV64
GOROOT
GOSUMDB
GOTMPDIR
diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go
index 278a89bd7..f51cdc708 100644
--- a/tpl/internal/go_templates/fmtsort/sort.go
+++ b/tpl/internal/go_templates/fmtsort/sort.go
@@ -9,25 +9,23 @@
package fmtsort
import (
+ "cmp"
"reflect"
- "sort"
+ "slices"
)
// 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
-}
+// SortedMap is a slice of KeyValue pairs that simplifies sorting
+// and iterating over map entries.
+//
+// Each KeyValue pair contains a map key and its corresponding value.
+type SortedMap []KeyValue
-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]
+// KeyValue holds a single key and value pair found in a map.
+type KeyValue struct {
+ Key, Value reflect.Value
}
// Sort accepts a map and returns a SortedMap that has the same keys and
@@ -48,7 +46,7 @@ func (o *SortedMap) Swap(i, j int) {
// 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 {
+func Sort(mapValue reflect.Value) SortedMap {
if mapValue.Type().Kind() != reflect.Map {
return nil
}
@@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
// of a concurrent map update. The runtime is responsible for
// yelling loudly if that happens. See issue 33275.
n := mapValue.Len()
- key := make([]reflect.Value, 0, n)
- value := make([]reflect.Value, 0, n)
+ sorted := make(SortedMap, 0, n)
iter := mapValue.MapRange()
for iter.Next() {
- key = append(key, iter.Key())
- value = append(value, iter.Value())
- }
- sorted := &SortedMap{
- Key: key,
- Value: value,
+ sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
}
- sort.Stable(sorted)
+ slices.SortStableFunc(sorted, func(a, b KeyValue) int {
+ return compare(a.Key, b.Key)
+ })
return sorted
}
@@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
}
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
- }
+ return cmp.Compare(aVal.Int(), bVal.Int())
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
- }
+ return cmp.Compare(aVal.Uint(), bVal.Uint())
case reflect.String:
- a, b := aVal.String(), bVal.String()
- switch {
- case a < b:
- return -1
- case a > b:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.String(), bVal.String())
case reflect.Float32, reflect.Float64:
- return floatCompare(aVal.Float(), bVal.Float())
+ return cmp.Compare(aVal.Float(), bVal.Float())
case reflect.Complex64, reflect.Complex128:
a, b := aVal.Complex(), bVal.Complex()
- if c := floatCompare(real(a), real(b)); c != 0 {
+ if c := cmp.Compare(real(a), real(b)); c != 0 {
return c
}
- return floatCompare(imag(a), imag(b))
+ return cmp.Compare(imag(a), imag(b))
case reflect.Bool:
a, b := aVal.Bool(), bVal.Bool()
switch {
@@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
return -1
}
case reflect.Pointer, reflect.UnsafePointer:
- a, b := aVal.Pointer(), bVal.Pointer()
- switch {
- case a < b:
- return -1
- case a > b:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.Pointer(), bVal.Pointer())
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
- }
+ return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ {
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
@@ -198,22 +152,3 @@ func nilCompare(aVal, bVal reflect.Value) (int, bool) {
}
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
-}
diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go
index e86b4c673..0986dbb6d 100644
--- a/tpl/internal/go_templates/fmtsort/sort_test.go
+++ b/tpl/internal/go_templates/fmtsort/sort_test.go
@@ -5,12 +5,13 @@
package fmtsort_test
import (
+ "cmp"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math"
"reflect"
"runtime"
- "sort"
+ "slices"
"strings"
"testing"
"unsafe"
@@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
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:
@@ -142,13 +139,13 @@ func sprint(data any) string {
return "nil"
}
b := new(strings.Builder)
- for i, key := range om.Key {
+ for i, m := range om {
if i > 0 {
b.WriteRune(' ')
}
- b.WriteString(sprintKey(key))
+ b.WriteString(sprintKey(m.Key))
b.WriteRune(':')
- fmt.Fprint(b, om.Value[i])
+ fmt.Fprint(b, m.Value)
}
return b.String()
}
@@ -200,8 +197,8 @@ func makeChans() []chan int {
for i := range cs {
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
}
- sort.Slice(cs, func(i, j int) bool {
- return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
+ slices.SortFunc(cs, func(a, b chan int) int {
+ return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
})
return cs
}
diff --git a/tpl/internal/go_templates/htmltemplate/content.go b/tpl/internal/go_templates/htmltemplate/content.go
index 9c61cfac0..d19b1ec12 100644
--- a/tpl/internal/go_templates/htmltemplate/content.go
+++ b/tpl/internal/go_templates/htmltemplate/content.go
@@ -29,7 +29,6 @@ const (
// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
-// Signature modified by Hugo. TODO(bep) script this.
func doIndirect(a any) any {
if a == nil {
return nil
@@ -46,8 +45,8 @@ func doIndirect(a any) any {
}
var (
- errorType = reflect.TypeOf((*error)(nil)).Elem()
- fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
+ errorType = reflect.TypeFor[error]()
+ fmtStringerType = reflect.TypeFor[fmt.Stringer]()
)
// indirectToStringerOrError returns the value, after dereferencing as many times
diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go
index 6be5e0f84..7442194f7 100644
--- a/tpl/internal/go_templates/htmltemplate/doc.go
+++ b/tpl/internal/go_templates/htmltemplate/doc.go
@@ -232,11 +232,9 @@ Least Surprise Property:
knows that contextual autoescaping happens should be able to look at a {{.}}
and correctly infer what sanitization happens."
-As a consequence of the Least Surprise Property, template actions within an
-ECMAScript 6 template literal are disabled by default.
-Handling string interpolation within these literals is rather complex resulting
-in no clear safe way to support it.
-To re-enable template actions within ECMAScript 6 template literals, use the
-GODEBUG=jstmpllitinterp=1 environment variable.
+Previously, ECMAScript 6 template literal were disabled by default, and could be
+enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
+literals are now supported by default, and setting jstmpllitinterp has no
+effect.
*/
package template
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 428cddc0d..e01813e68 100644
--- a/tpl/internal/go_templates/htmltemplate/exec_test.go
+++ b/tpl/internal/go_templates/htmltemplate/exec_test.go
@@ -273,8 +273,8 @@ type execTest struct {
// of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits.
var (
- bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
- bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
+ bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
+ bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
)
var execTests = []execTest{
@@ -580,6 +580,8 @@ var execTests = []execTest{
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
+ {"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
+ {"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
// Range.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go
index cc80d2b64..1b56abeb1 100644
--- a/tpl/internal/go_templates/htmltemplate/js.go
+++ b/tpl/internal/go_templates/htmltemplate/js.go
@@ -125,7 +125,7 @@ var regexpPrecederKeywords = map[string]bool{
"void": true,
}
-var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
+var jsonMarshalType = reflect.TypeFor[json.Marshaler]()
// indirectToJSONMarshaler returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
@@ -172,7 +172,7 @@ func jsValEscaper(args ...any) string {
// cyclic data. This may be an unacceptable DoS risk.
b, err := json.Marshal(a)
if err != nil {
- // While the standard JSON marshaller does not include user controlled
+ // While the standard JSON marshaler does not include user controlled
// information in the error message, if a type has a MarshalJSON method,
// the content of the error message is not guaranteed. Since we insert
// the error into the template, as part of a comment, we attempt to
@@ -393,7 +393,6 @@ var jsStrNormReplacementTable = []string{
'<': `\u003c`,
'>': `\u003e`,
}
-
var jsRegexpReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,
diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go
index d7454f101..4582ddd5f 100644
--- a/tpl/internal/go_templates/htmltemplate/template.go
+++ b/tpl/internal/go_templates/htmltemplate/template.go
@@ -179,7 +179,7 @@ func (t *Template) DefinedTemplates() string {
// 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.
+// before the first use of [Template.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
@@ -238,8 +238,8 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error
// 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
+// associated templates is, so further calls to [Template.Parse] in the copy will add
+// templates to the copy but not to the original. [Template.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.
//
@@ -342,7 +342,7 @@ func (t *Template) Funcs(funcMap FuncMap) *Template {
}
// Delims sets the action delimiters to the specified strings, to be used in
-// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
+// subsequent calls to [Template.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.
@@ -359,7 +359,7 @@ func (t *Template) Lookup(name string) *Template {
return t.set[name]
}
-// Must is a helper that wraps a call to a function returning (*Template, error)
+// 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
//
@@ -371,10 +371,10 @@ func Must(t *Template, err error) *Template {
return t
}
-// ParseFiles creates a new Template and parses the template definitions from
+// 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.
+// 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.
@@ -436,12 +436,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
return t, nil
}
-// ParseGlob creates a new Template and parses the template definitions from
+// 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.
+// [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.
@@ -485,7 +485,7 @@ func IsTrue(val any) (truth, ok bool) {
return template.IsTrue(val)
}
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
+// ParseFS is like [ParseFiles] or [ParseGlob] but reads from the file system fs
// instead of the host operating system's file system.
// It accepts a list of glob patterns.
// (Note that most file names serve as glob patterns matching only themselves.)
@@ -493,7 +493,7 @@ func ParseFS(fs fs.FS, patterns ...string) (*Template, error) {
return parseFS(nil, fs, patterns)
}
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fs
// instead of the host operating system's file system.
// It accepts a list of glob patterns.
// (Note that most file names serve as glob patterns matching only themselves.)
diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go
index d5a05f66d..c430389a3 100644
--- a/tpl/internal/go_templates/htmltemplate/transition.go
+++ b/tpl/internal/go_templates/htmltemplate/transition.go
@@ -414,7 +414,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
// If "</script" appears in a regex literal, the '/' should not
// close the regex literal, and it will later be escaped to
// "\x3C/script" in escapeText.
- if i > 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte("</script")) == 0 {
+ if i > 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
i++
} else if !inCharset {
c.state, c.jsCtx = stateJS, jsCtxDivOp
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
index 277df2255..e8577d2ea 100644
--- a/tpl/internal/go_templates/testenv/testenv.go
+++ b/tpl/internal/go_templates/testenv/testenv.go
@@ -132,15 +132,13 @@ func findGOROOT() (string, error) {
// If runtime.GOROOT() is non-empty, assume that it is valid.
//
// (It might not be: for example, the user may have explicitly set GOROOT
- // to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
- // and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
+ // to the wrong directory. But this case is
// rare, and if that happens the user can fix what they broke.)
return
}
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
- // binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
- // without GOROOT and the tree hasn't been moved there yet).
+ // binary was built with -trimpath).
//
// Since this is internal/testenv, we can cheat and assume that the caller
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
@@ -315,12 +313,18 @@ func MustInternalLink(t testing.TB, withCgo bool) {
}
}
+// MustInternalLinkPIE checks whether the current system can link PIE binary using
+// internal linking.
+// If not, MustInternalLinkPIE calls t.Skip with an explanation.
+// Modified by Hugo (not needed)
+func MustInternalLinkPIE(t testing.TB) {
+}
+
// MustHaveBuildMode reports whether the current system can build programs in
// the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation.
// Modified by Hugo (not needed)
func MustHaveBuildMode(t testing.TB, buildmode string) {
- return
}
// HasSymlink reports whether the current system can use os.Symlink.
@@ -447,3 +451,10 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
func SyscallIsNotSupported(err error) bool {
return syscallIsNotSupported(err)
}
+
+// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
+// This function should be used when it is necessary to avoid t.Parallel on
+// 32-bit machines, typically because the test uses lots of memory.
+// Disabled by Hugo.
+func ParallelOn64Bit(t *testing.T) {
+}
diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go
index 032784bc3..b3ffaabb1 100644
--- a/tpl/internal/go_templates/texttemplate/doc.go
+++ b/tpl/internal/go_templates/texttemplate/doc.go
@@ -144,6 +144,13 @@ data, defined in detail in the corresponding sections that follow.
is executed; otherwise, dot is set to the value of the pipeline
and T1 is executed.
+ {{with pipeline}} T1 {{else with pipeline}} T0 {{end}}
+ To simplify the appearance of with-else chains, the else action
+ of a with may include another with directly; the effect is exactly
+ the same as writing
+ {{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}}
+
+
Arguments
An argument is a simple value, denoted by one of the following.
diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go
index 295a810b8..975ceea93 100644
--- a/tpl/internal/go_templates/texttemplate/example_test.go
+++ b/tpl/internal/go_templates/texttemplate/example_test.go
@@ -35,7 +35,7 @@ Josie
Name, Gift string
Attended bool
}
- var recipients = []Recipient{
+ recipients := []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},
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/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
index 73153c764..bd8c82bd7 100644
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -7,13 +7,12 @@ package template
import (
"errors"
"fmt"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"io"
"reflect"
"runtime"
"strings"
-
- "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
// maxExecDepth specifies the maximum stack depth of templates within
@@ -95,7 +94,7 @@ type missingValType struct{}
var missingVal = reflect.ValueOf(missingValType{})
-var missingValReflectType = reflect.TypeOf(missingValType{})
+var missingValReflectType = reflect.TypeFor[missingValType]()
func isMissing(v reflect.Value) bool {
return v.IsValid() && v.Type() == missingValReflectType
@@ -202,8 +201,8 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error {
// 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.
+// 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 any) error {
return t.execute(wr, data)
}
@@ -229,7 +228,7 @@ func (t *Template) execute(wr io.Writer, data any) (err error) {
// 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.
+// and in [html/template].
func (t *Template) DefinedTemplates() string {
if t.common == nil {
return ""
@@ -409,8 +408,8 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
break
}
om := fmtsort.Sort(val)
- for i, key := range om.Key {
- oneIteration(key, om.Value[i])
+ for _, m := range om {
+ oneIteration(m.Key, m.Value)
}
return
case reflect.Chan:
@@ -480,7 +479,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
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!
+ value = value.Elem()
}
}
for _, variable := range pipe.Decl {
@@ -709,9 +708,9 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
}
var (
- errorType = reflect.TypeOf((*error)(nil)).Elem()
- fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
- reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
+ errorType = reflect.TypeFor[error]()
+ fmtStringerType = reflect.TypeFor[fmt.Stringer]()
+ reflectValueType = reflect.TypeFor[reflect.Value]()
)
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
@@ -735,9 +734,8 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
} 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())
+ if err := goodFunc(name, typ); err != nil {
+ s.errorf("%v", err)
}
unwrap := func(v reflect.Value) reflect.Value {
@@ -801,6 +799,15 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
}
argv[i] = s.validateType(final, t)
}
+
+ // Special case for the "call" builtin.
+ // Insert the name of the callee function as the first argument.
+ if isBuiltin && name == "call" {
+ calleeName := args[0].String()
+ argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
+ fun = reflect.ValueOf(call)
+ }
+
v, err := safeCall(fun, argv)
// If we have an error that is not nil, stop execution and return that
// error to the caller.
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index ed75adacb..efbaa9eec 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+//go:build !windows
+// +build !windows
+
package template
import (
@@ -75,12 +78,15 @@ type T struct {
PSI *[]int
NIL *int
// Function (not method)
- BinaryFunc func(string, string) string
- VariadicFunc func(...string) string
- VariadicFuncInt func(int, ...string) string
- NilOKFunc func(*int) bool
- ErrFunc func() (string, error)
- PanicFunc func() string
+ BinaryFunc func(string, string) string
+ VariadicFunc func(...string) string
+ VariadicFuncInt func(int, ...string) string
+ NilOKFunc func(*int) bool
+ ErrFunc func() (string, error)
+ PanicFunc func() string
+ TooFewReturnCountFunc func()
+ TooManyReturnCountFunc func() (string, error, int)
+ InvalidReturnTypeFunc func() (string, bool)
// Template to test evaluation of templates.
Tmpl *Template
// Unexported field; cannot be accessed by template.
@@ -168,6 +174,9 @@ var tVal = &T{
NilOKFunc: func(s *int) bool { return s == nil },
ErrFunc: func() (string, error) { return "bla", nil },
PanicFunc: func() string { panic("test panic") },
+ TooFewReturnCountFunc: func() {},
+ TooManyReturnCountFunc: func() (string, error, int) { return "", nil, 0 },
+ InvalidReturnTypeFunc: func() (string, bool) { return "", false },
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
}
@@ -265,8 +274,8 @@ type execTest struct {
// of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits.
var (
- bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
- bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
+ bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
+ bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
)
var execTests = []execTest{
@@ -583,6 +592,8 @@ var execTests = []execTest{
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
+ {"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
+ {"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
// Range.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
@@ -1723,6 +1734,81 @@ func TestExecutePanicDuringCall(t *testing.T) {
}
}
+func TestFunctionCheckDuringCall(t *testing.T) {
+ tests := []struct {
+ name string
+ 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 non-function",
+ input: "{{call .True}}",
+ data: tVal,
+ wantErr: "error calling call: non-function .True of type bool",
+ },
+ {
+ name: "call func with wrong argument",
+ input: "{{call .BinaryFunc 1}}",
+ data: tVal,
+ wantErr: "error calling call: wrong number of args for .BinaryFunc: got 1 want 2",
+ },
+ {
+ name: "call variadic func with wrong argument",
+ input: `{{call .VariadicFuncInt}}`,
+ data: tVal,
+ wantErr: "error calling call: wrong number of args for .VariadicFuncInt: got 0 want at least 1",
+ },
+ {
+ name: "call too few return number func",
+ input: `{{call .TooFewReturnCountFunc}}`,
+ data: tVal,
+ wantErr: "error calling call: function .TooFewReturnCountFunc has 0 return values; should be 1 or 2",
+ },
+ {
+ name: "call too many return number func",
+ input: `{{call .TooManyReturnCountFunc}}`,
+ data: tVal,
+ wantErr: "error calling call: function .TooManyReturnCountFunc has 3 return values; should be 1 or 2",
+ },
+ {
+ name: "call invalid return type func",
+ input: `{{call .InvalidReturnTypeFunc}}`,
+ data: tVal,
+ wantErr: "error calling call: invalid function signature for .InvalidReturnTypeFunc: second return value should be error; is bool",
+ },
+ {
+ name: "call pipeline",
+ input: `{{call (len "test")}}`,
+ data: nil,
+ wantErr: "error calling call: non-function len \"test\" of type int",
+ },
+ }
+
+ for _, tc := range tests {
+ b := new(bytes.Buffer)
+ tmpl, err := New("t").Parse(tc.input)
+ if err != nil {
+ t.Fatalf("parse error: %s", err)
+ }
+ err = tmpl.Execute(b, tc.data)
+ if err == nil {
+ t.Errorf("%s: expected error; got none", tc.name)
+ } else if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) {
+ if *debug {
+ fmt.Printf("%s: test execute error: %s\n", tc.name, err)
+ }
+ t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err)
+ }
+ }
+}
+
// Issue 31810. Check that a parenthesized first argument behaves properly.
func TestIssue31810(t *testing.T) {
// A simple value with no arguments is fine.
diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go
index a949f896f..7d63cf8b7 100644
--- a/tpl/internal/go_templates/texttemplate/funcs.go
+++ b/tpl/internal/go_templates/texttemplate/funcs.go
@@ -22,14 +22,14 @@ import (
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
//
-// Errors returned by Execute wrap the underlying error; call errors.As to
+// Errors returned by Execute wrap the underlying error; call [errors.As] to
// unwrap them.
//
// 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.
+// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
+// type can return interface{} or [reflect.Value].
type FuncMap map[string]any
// builtins returns the FuncMap.
@@ -39,7 +39,7 @@ type FuncMap map[string]any
func builtins() FuncMap {
return FuncMap{
"and": and,
- "call": call,
+ "call": emptyCall,
"html": HTMLEscaper,
"index": index,
"slice": slice,
@@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
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()))
+ if err := goodFunc(name, v.Type()); err != nil {
+ panic(err)
}
out[name] = v
}
@@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
}
// goodFunc reports whether the function or method has the right result signature.
-func goodFunc(typ reflect.Type) bool {
+func goodFunc(name string, typ reflect.Type) error {
// 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
+ switch numOut := typ.NumOut(); {
+ case numOut == 1:
+ return nil
+ case numOut == 2 && typ.Out(1) == errorType:
+ return nil
+ case numOut == 2:
+ return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1))
+ default:
+ return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut())
}
- return false
}
// goodName reports whether the function name is a valid identifier.
@@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
// Function invocation
+func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value {
+ panic("unreachable") // implemented as a special case in evalCall
+}
+
// 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) {
+func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
fn = indirectInterface(fn)
if !fn.IsValid() {
return reflect.Value{}, fmt.Errorf("call of nil")
}
typ := fn.Type()
if typ.Kind() != reflect.Func {
- return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
+ return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ)
}
- if !goodFunc(typ) {
- return reflect.Value{}, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
+
+ if err := goodFunc(name, typ); err != nil {
+ return reflect.Value{}, err
}
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)
+ return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, 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)
+ return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn)
}
}
argv := make([]reflect.Value, len(args))
diff --git a/tpl/internal/go_templates/texttemplate/helper.go b/tpl/internal/go_templates/texttemplate/helper.go
index 48af3928b..81b55538e 100644
--- a/tpl/internal/go_templates/texttemplate/helper.go
+++ b/tpl/internal/go_templates/texttemplate/helper.go
@@ -16,7 +16,7 @@ import (
// Functions and methods to parse templates.
-// Must is a helper that wraps a call to a function returning (*Template, error)
+// 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
//
@@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
return t
}
-// ParseFiles creates a new Template and parses the template definitions from
+// 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.
@@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
// 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
+// (see [filepath.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,
@@ -93,12 +93,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
return t, nil
}
-// ParseGlob creates a new Template and parses the template definitions from
+// 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.
+// semantics of [filepath.Match], and the pattern must match at least one file.
+// The returned template will have the [filepath.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.
@@ -108,9 +108,9 @@ func ParseGlob(pattern string) (*Template, error) {
// 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.
+// according to the semantics of [filepath.Match], and the pattern must match at
+// least one file. ParseGlob is equivalent to calling [Template.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.
@@ -131,17 +131,17 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
return parseFiles(t, readFileOS, filenames...)
}
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
// instead of the host operating system's file system.
-// It accepts a list of glob patterns.
+// It accepts a list of glob patterns (see [path.Match]).
// (Note that most file names serve as glob patterns matching only themselves.)
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
return parseFS(nil, fsys, patterns)
}
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
// instead of the host operating system's file system.
-// It accepts a list of glob patterns.
+// It accepts a list of glob patterns (see [path.Match]).
// (Note that most file names serve as glob patterns matching only themselves.)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
t.init()
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index 276367a7c..12dbe0412 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -278,9 +278,8 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
} 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())
+ if err := goodFunc(name, typ); err != nil {
+ s.errorf("%v", err)
}
unwrap := func(v reflect.Value) reflect.Value {
@@ -345,6 +344,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
argv[i] = s.validateType(final, t)
}
+ // Special case for the "call" builtin.
+ // Insert the name of the callee function as the first argument.
+ if isBuiltin && name == "call" {
+ calleeName := args[0].String()
+ argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
+ fun = reflect.ValueOf(call)
+ }
+
// Added for Hugo
for i := 0; i < len(first); i++ {
argv[i] = s.validateType(first[i], typ.In(i))
diff --git a/tpl/internal/go_templates/texttemplate/link_test.go b/tpl/internal/go_templates/texttemplate/link_test.go
index 23f6a31fa..63418cd90 100644
--- a/tpl/internal/go_templates/texttemplate/link_test.go
+++ b/tpl/internal/go_templates/texttemplate/link_test.go
@@ -2,18 +2,16 @@
// 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 (
"bytes"
- "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
"os"
"os/exec"
"path/filepath"
"testing"
+
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
)
// Issue 36021: verify that text/template doesn't prevent the linker from removing
@@ -44,7 +42,7 @@ func main() {
`
td := t.TempDir()
- if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
t.Fatal(err)
}
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go
index c36688825..a31309874 100644
--- a/tpl/internal/go_templates/texttemplate/parse/node.go
+++ b/tpl/internal/go_templates/texttemplate/parse/node.go
@@ -217,7 +217,11 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
}
v.writeTo(sb)
}
- sb.WriteString(" := ")
+ if p.IsAssign {
+ sb.WriteString(" = ")
+ } else {
+ sb.WriteString(" := ")
+ }
}
for i, c := range p.Cmds {
if i > 0 {
@@ -346,12 +350,12 @@ type IdentifierNode struct {
Ident string // The identifier's name.
}
-// NewIdentifier returns a new IdentifierNode with the given identifier 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.
+// 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 {
@@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
return i
}
-// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
+// 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 {
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
index d43d5334b..27c84f31e 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse.go
@@ -42,7 +42,7 @@ const (
SkipFuncCheck // do not check that functions are defined
)
-// Copy returns a copy of the Tree. Any parsing state is discarded.
+// Copy returns a copy of the [Tree]. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
if t == nil {
return nil
@@ -55,7 +55,7 @@ func (t *Tree) Copy() *Tree {
}
}
-// Parse returns a map from template name to parse.Tree, created by parsing the
+// Parse returns a map from template name to [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.
@@ -521,7 +521,7 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
}
}
-func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
+func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
defer t.popVars(len(t.vars))
pipe = t.pipeline(context, itemRightDelim)
if context == "range" {
@@ -535,27 +535,30 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
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
+ // Special case for "else if" and "else with".
+ // If the "else" is followed immediately by an "if" or "with",
+ // the elseControl will have left the "if" or "with" token pending. Treat
+ // {{if a}}_{{else if b}}_{{end}}
+ // {{with a}}_{{else with b}}_{{end}}
+ // as
+ // {{if a}}_{{else}}{{if b}}_{{end}}{{end}}
+ // {{with a}}_{{else}}{{with b}}_{{end}}{{end}}.
+ // To do this, parse the "if" or "with" as usual and stop at it {{end}};
+ // the subsequent{{end}} is assumed. This technique works even for long if-else-if chains.
+ if context == "if" && t.peek().typ == itemIf {
+ t.next() // Consume the "if" token.
+ elseList = t.newList(next.Position())
+ elseList.append(t.ifControl())
+ } else if context == "with" && t.peek().typ == itemWith {
+ t.next()
+ elseList = t.newList(next.Position())
+ elseList.append(t.withControl())
+ } else {
+ elseList, next = t.itemList()
+ if next.Type() != nodeEnd {
+ t.errorf("expected end; found %s", next)
}
}
- elseList, next = t.itemList()
- if next.Type() != nodeEnd {
- t.errorf("expected end; found %s", next)
- }
}
return pipe.Position(), pipe.Line, pipe, list, elseList
}
@@ -567,7 +570,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
//
// If keyword is past.
func (t *Tree) ifControl() Node {
- return t.newIf(t.parseControl(true, "if"))
+ return t.newIf(t.parseControl("if"))
}
// Range:
@@ -577,7 +580,7 @@ func (t *Tree) ifControl() Node {
//
// Range keyword is past.
func (t *Tree) rangeControl() Node {
- r := t.newRange(t.parseControl(false, "range"))
+ r := t.newRange(t.parseControl("range"))
return r
}
@@ -588,7 +591,7 @@ func (t *Tree) rangeControl() Node {
//
// If keyword is past.
func (t *Tree) withControl() Node {
- return t.newWith(t.parseControl(false, "with"))
+ return t.newWith(t.parseControl("with"))
}
// End:
@@ -606,10 +609,11 @@ func (t *Tree) endControl() Node {
//
// 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 ... ".
+ // The "{{else if ... " and "{{else with ..." will be
+ // treated as "{{else}}{{if ..." and "{{else}}{{with ...".
+ // So return the else node here.
+ if peek.typ == itemIf || peek.typ == itemWith {
return t.newElse(peek.pos, peek.line)
}
token := t.expect(itemRightDelim, "else")
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
index 59e0a1741..80e7f53fa 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
@@ -33,9 +33,9 @@ var numberTests = []numberTest{
{"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},
+ {"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
+ {"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
+ {"0O73", true, true, true, false, 0o73, 0o73, 0o73, 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},
@@ -61,7 +61,7 @@ var numberTests = []numberTest{
{"-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},
+ {"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
{"-0x0", true, true, true, false, 0, 0, 0, 0},
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
// character constants
@@ -176,74 +176,150 @@ const (
)
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}}`},
- {"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
- `{{range .SI}}{{.}}{{break}}{{end}}`},
- {"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
- `{{range .SI}}{{.}}{{continue}}{{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}}`},
+ {
+ "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}}`,
+ },
+ {
+ "range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
+ `{{range .SI}}{{.}}{{break}}{{end}}`,
+ },
+ {
+ "range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
+ `{{range .SI}}{{.}}{{continue}}{{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}}`,
+ },
+ {
+ "with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
+ `{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`,
+ },
+ {
+ "with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
+ `{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`,
+ },
// Trimming spaces.
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
@@ -252,18 +328,24 @@ var parseTests = []parseTest{
{"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" .}}`},
+ {
+ "block definition", `{{block "foo" .}}hello{{end}}`, noError,
+ `{{template "foo" .}}`,
+ },
{"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
- {"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
- `{{range .SI}}{{.}}{{continue}}{{end}}`},
- {"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
- `{{range .SI}}{{.}}{{break}}{{end}}`},
+ {
+ "spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
+ `{{range .SI}}{{.}}{{continue}}{{end}}`,
+ },
+ {
+ "spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
+ `{{range .SI}}{{.}}{{break}}{{end}}`,
+ },
// Errors.
{"unclosed action", "hello{{range", hasError, ""},
@@ -302,6 +384,9 @@ var parseTests = []parseTest{
{"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.
+ // Check the range handles assignment vs. declaration properly.
+ {"bug2a", "{{range $x := 0}}{{$x}}{{end}}", noError, "{{range $x := 0}}{{$x}}{{end}}"},
+ {"bug2b", "{{range $x = 0}}{{$x}}{{end}}", noError, "{{range $x = 0}}{{$x}}{{end}}"},
// dot following a literal value
{"dot after integer", "{{1.E}}", hasError, ""},
{"dot after float", "{{0.1.E}}", hasError, ""},
@@ -402,7 +487,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
{
// 'break' is a defined function, don't treat it as a keyword: it should
// accept an argument successfully.
- var funcsWithKeywordFunc = map[string]any{
+ funcsWithKeywordFunc := map[string]any{
"break": func(in any) any { return in },
}
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
@@ -489,104 +574,168 @@ func TestErrorContextWithTreeCopy(t *testing.T) {
// All failures, and the result is a string that must appear in the error message.
var errorTests = []parseTest{
// Check line numbers are accurate.
- {"unclosed1",
+ {
+ "unclosed1",
"line1\n{{",
- hasError, `unclosed1:2: unclosed action`},
- {"unclosed2",
+ hasError, `unclosed1:2: unclosed action`,
+ },
+ {
+ "unclosed2",
"line1\n{{define `x`}}line2\n{{",
- hasError, `unclosed2:3: unclosed action`},
- {"unclosed3",
+ hasError, `unclosed2:3: unclosed action`,
+ },
+ {
+ "unclosed3",
"line1\n{{\"x\"\n\"y\"\n",
- hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
- {"unclosed4",
+ hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
+ },
+ {
+ "unclosed4",
"{{\n\n\n\n\n",
- hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
- {"var1",
+ hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
+ },
+ {
+ "var1",
"line1\n{{\nx\n}}",
- hasError, `var1:3: function "x" not defined`},
+ hasError, `var1:3: function "x" not defined`,
+ },
// Specific errors.
- {"function",
+ {
+ "function",
"{{foo}}",
- hasError, `function "foo" not defined`},
- {"comment1",
+ hasError, `function "foo" not defined`,
+ },
+ {
+ "comment1",
"{{/*}}",
- hasError, `comment1:1: unclosed comment`},
- {"comment2",
+ hasError, `comment1:1: unclosed comment`,
+ },
+ {
+ "comment2",
"{{/*\nhello\n}}",
- hasError, `comment2:1: unclosed comment`},
- {"lparen",
+ hasError, `comment2:1: unclosed comment`,
+ },
+ {
+ "lparen",
"{{.X (1 2 3}}",
- hasError, `unclosed left paren`},
- {"rparen",
+ hasError, `unclosed left paren`,
+ },
+ {
+ "rparen",
"{{.X 1 2 3 ) }}",
- hasError, "unexpected right paren"},
- {"rparen2",
+ hasError, "unexpected right paren",
+ },
+ {
+ "rparen2",
"{{(.X 1 2 3",
- hasError, `unclosed action`},
- {"space",
+ hasError, `unclosed action`,
+ },
+ {
+ "space",
"{{`x`3}}",
- hasError, `in operand`},
- {"idchar",
+ hasError, `in operand`,
+ },
+ {
+ "idchar",
"{{a#}}",
- hasError, `'#'`},
- {"charconst",
+ hasError, `'#'`,
+ },
+ {
+ "charconst",
"{{'a}}",
- hasError, `unterminated character constant`},
- {"stringconst",
+ hasError, `unterminated character constant`,
+ },
+ {
+ "stringconst",
`{{"a}}`,
- hasError, `unterminated quoted string`},
- {"rawstringconst",
+ hasError, `unterminated quoted string`,
+ },
+ {
+ "rawstringconst",
"{{`a}}",
- hasError, `unterminated raw quoted string`},
- {"number",
+ hasError, `unterminated raw quoted string`,
+ },
+ {
+ "number",
"{{0xi}}",
- hasError, `number syntax`},
- {"multidefine",
+ hasError, `number syntax`,
+ },
+ {
+ "multidefine",
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
- hasError, `multiple definition of template`},
- {"eof",
+ hasError, `multiple definition of template`,
+ },
+ {
+ "eof",
"{{range .X}}",
- hasError, `unexpected EOF`},
- {"variable",
+ 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",
+ hasError, `unexpected ":="`,
+ },
+ {
+ "multidecl",
"{{$a,$b,$c := 23}}",
- hasError, `too many declarations`},
- {"undefvar",
+ hasError, `too many declarations`,
+ },
+ {
+ "undefvar",
"{{$a}}",
- hasError, `undefined variable`},
- {"wrongdot",
+ hasError, `undefined variable`,
+ },
+ {
+ "wrongdot",
"{{true.any}}",
- hasError, `unexpected . after term`},
- {"wrongpipeline",
+ hasError, `unexpected . after term`,
+ },
+ {
+ "wrongpipeline",
"{{12|false}}",
- hasError, `non executable command in pipeline`},
- {"emptypipeline",
+ hasError, `non executable command in pipeline`,
+ },
+ {
+ "emptypipeline",
`{{ ( ) }}`,
- hasError, `missing value for parenthesized pipeline`},
- {"multilinerawstring",
+ hasError, `missing value for parenthesized pipeline`,
+ },
+ {
+ "multilinerawstring",
"{{ $v := `\n` }} {{",
- hasError, `multilinerawstring:2: unclosed action`},
- {"rangeundefvar",
+ hasError, `multilinerawstring:2: unclosed action`,
+ },
+ {
+ "rangeundefvar",
"{{range $k}}{{end}}",
- hasError, `undefined variable`},
- {"rangeundefvars",
+ hasError, `undefined variable`,
+ },
+ {
+ "rangeundefvars",
"{{range $k, $v}}{{end}}",
- hasError, `undefined variable`},
- {"rangemissingvalue1",
+ hasError, `undefined variable`,
+ },
+ {
+ "rangemissingvalue1",
"{{range $k,}}{{end}}",
- hasError, `missing value for range`},
- {"rangemissingvalue2",
+ hasError, `missing value for range`,
+ },
+ {
+ "rangemissingvalue2",
"{{range $k, $v := }}{{end}}",
- hasError, `missing value for range`},
- {"rangenotvariable1",
+ hasError, `missing value for range`,
+ },
+ {
+ "rangenotvariable1",
"{{range $k, .}}{{end}}",
- hasError, `range can only initialize variables`},
- {"rangenotvariable2",
+ hasError, `range can only initialize variables`,
+ },
+ {
+ "rangenotvariable2",
"{{range $k, 123 := .}}{{end}}",
- hasError, `range can only initialize variables`},
+ hasError, `range can only initialize variables`,
+ },
}
func TestErrors(t *testing.T) {
diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go
index 1ba72c194..536932a60 100644
--- a/tpl/internal/go_templates/texttemplate/template.go
+++ b/tpl/internal/go_templates/texttemplate/template.go
@@ -24,7 +24,7 @@ type common struct {
}
// Template is the representation of a parsed template. The *parse.Tree
-// field is exported only for use by html/template and should be treated
+// field is exported only for use by [html/template] and should be treated
// as unexported by all other clients.
type Template struct {
name string
@@ -79,7 +79,7 @@ func (t *Template) init() {
// 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
+// associated templates is, so further calls to [Template.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.
@@ -157,7 +157,7 @@ func (t *Template) Templates() []*Template {
}
// Delims sets the action delimiters to the specified strings, to be used in
-// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
+// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.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.
diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
index 41257912d..a8599bbad 100644
--- a/tpl/tplimpl/tplimpl_integration_test.go
+++ b/tpl/tplimpl/tplimpl_integration_test.go
@@ -116,6 +116,20 @@ counter2: 3
`)
}
+func TestGo23ElseWith(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo"
+-- layouts/index.html --
+{{ with false }}{{ else with .Site }}{{ .Title }}{{ end }}|
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Hugo|")
+}
+
// Issue 10495
func TestCommentsBeforeBlockDefinition(t *testing.T) {
t.Parallel()