mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
parent
b3ad58fa04
commit
2168c5b125
34 changed files with 616 additions and 402 deletions
|
@ -4,7 +4,7 @@ parameters:
|
||||||
defaults: &defaults
|
defaults: &defaults
|
||||||
resource_class: large
|
resource_class: large
|
||||||
docker:
|
docker:
|
||||||
- image: bepsays/ci-hugoreleaser:1.22200.20501
|
- image: bepsays/ci-hugoreleaser:1.22300.20000
|
||||||
environment: &buildenv
|
environment: &buildenv
|
||||||
GOMODCACHE: /root/project/gomodcache
|
GOMODCACHE: /root/project/gomodcache
|
||||||
version: 2
|
version: 2
|
||||||
|
@ -60,7 +60,7 @@ jobs:
|
||||||
environment:
|
environment:
|
||||||
<<: [*buildenv]
|
<<: [*buildenv]
|
||||||
docker:
|
docker:
|
||||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20501
|
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20000
|
||||||
steps:
|
steps:
|
||||||
- *restore-cache
|
- *restore-cache
|
||||||
- &attach-workspace
|
- &attach-workspace
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.21.x, 1.22.x]
|
go-version: [1.22.x, 1.23.x]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -170,4 +170,4 @@ require (
|
||||||
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
|
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.21.8
|
go 1.22.6
|
||||||
|
|
|
@ -147,7 +147,8 @@ func isWrite(flag int) bool {
|
||||||
// TODO(bep) move this to a more suitable place.
|
// TODO(bep) move this to a more suitable place.
|
||||||
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
|
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
|
||||||
// Safe guard
|
// 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))
|
panic(fmt.Sprint("invalid dir:", dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -365,18 +365,6 @@ func (c *Client) Get(args ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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)
|
return fmt.Errorf("failed to get %q: %w", args, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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.
|
// TODO(bep) preserve the staticcheck.conf file.
|
||||||
fmt.Println("Forking ...")
|
fmt.Println("Forking ...")
|
||||||
defer fmt.Println("Done ...")
|
defer fmt.Println("Done ...")
|
||||||
|
|
|
@ -18,7 +18,8 @@ hugo mod clean
|
||||||
! stderr .
|
! stderr .
|
||||||
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
|
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
|
||||||
hugo mod clean --all
|
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
|
cd submod
|
||||||
hugo mod init testsubmod
|
hugo mod init testsubmod
|
||||||
cmpenv go.mod $WORK/golden/go.mod.testsubmod
|
cmpenv go.mod $WORK/golden/go.mod.testsubmod
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
dostounix golden/package.json
|
dostounix golden/package.json
|
||||||
|
|
||||||
|
|
||||||
hugo mod npm pack
|
hugo mod npm pack
|
||||||
cmp package.json golden/package.json
|
cmp package.json golden/package.json
|
||||||
|
|
||||||
|
@ -41,3 +42,4 @@ path="github.com/gohugoio/hugoTestModule2"
|
||||||
}
|
}
|
||||||
-- go.mod --
|
-- go.mod --
|
||||||
module github.com/gohugoio/hugoTestModule
|
module github.com/gohugoio/hugoTestModule
|
||||||
|
go 1.20
|
||||||
|
|
|
@ -55,3 +55,4 @@ path="github.com/gohugoio/hugoTestModule2"
|
||||||
}
|
}
|
||||||
-- go.mod --
|
-- go.mod --
|
||||||
module github.com/gohugoio/hugoTestModule
|
module github.com/gohugoio/hugoTestModule
|
||||||
|
go 1.20
|
||||||
|
|
|
@ -36,6 +36,7 @@ const KnownEnv = `
|
||||||
GOAMD64
|
GOAMD64
|
||||||
GOARCH
|
GOARCH
|
||||||
GOARM
|
GOARM
|
||||||
|
GOARM64
|
||||||
GOBIN
|
GOBIN
|
||||||
GOCACHE
|
GOCACHE
|
||||||
GOCACHEPROG
|
GOCACHEPROG
|
||||||
|
@ -57,6 +58,7 @@ const KnownEnv = `
|
||||||
GOPPC64
|
GOPPC64
|
||||||
GOPRIVATE
|
GOPRIVATE
|
||||||
GOPROXY
|
GOPROXY
|
||||||
|
GORISCV64
|
||||||
GOROOT
|
GOROOT
|
||||||
GOSUMDB
|
GOSUMDB
|
||||||
GOTMPDIR
|
GOTMPDIR
|
||||||
|
|
|
@ -9,25 +9,23 @@
|
||||||
package fmtsort
|
package fmtsort
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Note: Throughout this package we avoid calling reflect.Value.Interface as
|
// 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.
|
// 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
|
// SortedMap is a slice of KeyValue pairs that simplifies sorting
|
||||||
// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
|
// and iterating over map entries.
|
||||||
type SortedMap struct {
|
//
|
||||||
Key []reflect.Value
|
// Each KeyValue pair contains a map key and its corresponding value.
|
||||||
Value []reflect.Value
|
type SortedMap []KeyValue
|
||||||
}
|
|
||||||
|
|
||||||
func (o *SortedMap) Len() int { return len(o.Key) }
|
// KeyValue holds a single key and value pair found in a map.
|
||||||
func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
|
type KeyValue struct {
|
||||||
func (o *SortedMap) Swap(i, j int) {
|
Key, Value reflect.Value
|
||||||
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
|
|
||||||
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort accepts a map and returns a SortedMap that has the same keys and
|
// 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.
|
// Otherwise identical arrays compare by length.
|
||||||
// - interface values compare first by reflect.Type describing the concrete type
|
// - interface values compare first by reflect.Type describing the concrete type
|
||||||
// and then by concrete value as described in the previous rules.
|
// 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 {
|
if mapValue.Type().Kind() != reflect.Map {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
|
||||||
// of a concurrent map update. The runtime is responsible for
|
// of a concurrent map update. The runtime is responsible for
|
||||||
// yelling loudly if that happens. See issue 33275.
|
// yelling loudly if that happens. See issue 33275.
|
||||||
n := mapValue.Len()
|
n := mapValue.Len()
|
||||||
key := make([]reflect.Value, 0, n)
|
sorted := make(SortedMap, 0, n)
|
||||||
value := make([]reflect.Value, 0, n)
|
|
||||||
iter := mapValue.MapRange()
|
iter := mapValue.MapRange()
|
||||||
for iter.Next() {
|
for iter.Next() {
|
||||||
key = append(key, iter.Key())
|
sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
|
||||||
value = append(value, iter.Value())
|
|
||||||
}
|
}
|
||||||
sorted := &SortedMap{
|
slices.SortStableFunc(sorted, func(a, b KeyValue) int {
|
||||||
Key: key,
|
return compare(a.Key, b.Key)
|
||||||
Value: value,
|
})
|
||||||
}
|
|
||||||
sort.Stable(sorted)
|
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
|
||||||
}
|
}
|
||||||
switch aVal.Kind() {
|
switch aVal.Kind() {
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
a, b := aVal.Int(), bVal.Int()
|
return cmp.Compare(aVal.Int(), bVal.Int())
|
||||||
switch {
|
|
||||||
case a < b:
|
|
||||||
return -1
|
|
||||||
case a > b:
|
|
||||||
return 1
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
a, b := aVal.Uint(), bVal.Uint()
|
return cmp.Compare(aVal.Uint(), bVal.Uint())
|
||||||
switch {
|
|
||||||
case a < b:
|
|
||||||
return -1
|
|
||||||
case a > b:
|
|
||||||
return 1
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
a, b := aVal.String(), bVal.String()
|
return cmp.Compare(aVal.String(), bVal.String())
|
||||||
switch {
|
|
||||||
case a < b:
|
|
||||||
return -1
|
|
||||||
case a > b:
|
|
||||||
return 1
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
return floatCompare(aVal.Float(), bVal.Float())
|
return cmp.Compare(aVal.Float(), bVal.Float())
|
||||||
case reflect.Complex64, reflect.Complex128:
|
case reflect.Complex64, reflect.Complex128:
|
||||||
a, b := aVal.Complex(), bVal.Complex()
|
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 c
|
||||||
}
|
}
|
||||||
return floatCompare(imag(a), imag(b))
|
return cmp.Compare(imag(a), imag(b))
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
a, b := aVal.Bool(), bVal.Bool()
|
a, b := aVal.Bool(), bVal.Bool()
|
||||||
switch {
|
switch {
|
||||||
|
@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
case reflect.Pointer, reflect.UnsafePointer:
|
case reflect.Pointer, reflect.UnsafePointer:
|
||||||
a, b := aVal.Pointer(), bVal.Pointer()
|
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
|
||||||
switch {
|
|
||||||
case a < b:
|
|
||||||
return -1
|
|
||||||
case a > b:
|
|
||||||
return 1
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
case reflect.Chan:
|
case reflect.Chan:
|
||||||
if c, ok := nilCompare(aVal, bVal); ok {
|
if c, ok := nilCompare(aVal, bVal); ok {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
ap, bp := aVal.Pointer(), bVal.Pointer()
|
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
|
||||||
switch {
|
|
||||||
case ap < bp:
|
|
||||||
return -1
|
|
||||||
case ap > bp:
|
|
||||||
return 1
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
for i := 0; i < aVal.NumField(); i++ {
|
for i := 0; i < aVal.NumField(); i++ {
|
||||||
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
|
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
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
package fmtsort_test
|
package fmtsort_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
|
||||||
switch {
|
switch {
|
||||||
case i == j:
|
case i == j:
|
||||||
expect = 0
|
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:
|
case i < j:
|
||||||
expect = -1
|
expect = -1
|
||||||
case i > j:
|
case i > j:
|
||||||
|
@ -142,13 +139,13 @@ func sprint(data any) string {
|
||||||
return "nil"
|
return "nil"
|
||||||
}
|
}
|
||||||
b := new(strings.Builder)
|
b := new(strings.Builder)
|
||||||
for i, key := range om.Key {
|
for i, m := range om {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
b.WriteRune(' ')
|
b.WriteRune(' ')
|
||||||
}
|
}
|
||||||
b.WriteString(sprintKey(key))
|
b.WriteString(sprintKey(m.Key))
|
||||||
b.WriteRune(':')
|
b.WriteRune(':')
|
||||||
fmt.Fprint(b, om.Value[i])
|
fmt.Fprint(b, m.Value)
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
@ -200,8 +197,8 @@ func makeChans() []chan int {
|
||||||
for i := range cs {
|
for i := range cs {
|
||||||
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
|
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
|
||||||
}
|
}
|
||||||
sort.Slice(cs, func(i, j int) bool {
|
slices.SortFunc(cs, func(a, b chan int) int {
|
||||||
return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
|
return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
|
||||||
})
|
})
|
||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ const (
|
||||||
|
|
||||||
// indirect returns the value, after dereferencing as many times
|
// indirect returns the value, after dereferencing as many times
|
||||||
// as necessary to reach the base type (or nil).
|
// as necessary to reach the base type (or nil).
|
||||||
// Signature modified by Hugo. TODO(bep) script this.
|
|
||||||
func doIndirect(a any) any {
|
func doIndirect(a any) any {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -46,8 +45,8 @@ func doIndirect(a any) any {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
errorType = reflect.TypeFor[error]()
|
||||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
fmtStringerType = reflect.TypeFor[fmt.Stringer]()
|
||||||
)
|
)
|
||||||
|
|
||||||
// indirectToStringerOrError returns the value, after dereferencing as many times
|
// indirectToStringerOrError returns the value, after dereferencing as many times
|
||||||
|
|
|
@ -232,11 +232,9 @@ Least Surprise Property:
|
||||||
knows that contextual autoescaping happens should be able to look at a {{.}}
|
knows that contextual autoescaping happens should be able to look at a {{.}}
|
||||||
and correctly infer what sanitization happens."
|
and correctly infer what sanitization happens."
|
||||||
|
|
||||||
As a consequence of the Least Surprise Property, template actions within an
|
Previously, ECMAScript 6 template literal were disabled by default, and could be
|
||||||
ECMAScript 6 template literal are disabled by default.
|
enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
|
||||||
Handling string interpolation within these literals is rather complex resulting
|
literals are now supported by default, and setting jstmpllitinterp has no
|
||||||
in no clear safe way to support it.
|
effect.
|
||||||
To re-enable template actions within ECMAScript 6 template literals, use the
|
|
||||||
GODEBUG=jstmpllitinterp=1 environment variable.
|
|
||||||
*/
|
*/
|
||||||
package template
|
package template
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build go1.13
|
|
||||||
// +build go1.13
|
|
||||||
|
|
||||||
package template_test
|
package template_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -273,8 +273,8 @@ type execTest struct {
|
||||||
// of the max int boundary.
|
// of the max int boundary.
|
||||||
// We do it this way so the test doesn't depend on ints being 32 bits.
|
// We do it this way so the test doesn't depend on ints being 32 bits.
|
||||||
var (
|
var (
|
||||||
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
|
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
|
||||||
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
|
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
|
||||||
)
|
)
|
||||||
|
|
||||||
var execTests = []execTest{
|
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 $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 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 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.
|
||||||
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
|
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
|
||||||
|
|
|
@ -125,7 +125,7 @@ var regexpPrecederKeywords = map[string]bool{
|
||||||
"void": true,
|
"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
|
// indirectToJSONMarshaler returns the value, after dereferencing as many times
|
||||||
// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
|
// 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.
|
// cyclic data. This may be an unacceptable DoS risk.
|
||||||
b, err := json.Marshal(a)
|
b, err := json.Marshal(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 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,
|
// 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 content of the error message is not guaranteed. Since we insert
|
||||||
// the error into the template, as part of a comment, we attempt to
|
// the error into the template, as part of a comment, we attempt to
|
||||||
|
@ -393,7 +393,6 @@ var jsStrNormReplacementTable = []string{
|
||||||
'<': `\u003c`,
|
'<': `\u003c`,
|
||||||
'>': `\u003e`,
|
'>': `\u003e`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsRegexpReplacementTable = []string{
|
var jsRegexpReplacementTable = []string{
|
||||||
0: `\u0000`,
|
0: `\u0000`,
|
||||||
'\t': `\t`,
|
'\t': `\t`,
|
||||||
|
|
|
@ -179,7 +179,7 @@ func (t *Template) DefinedTemplates() string {
|
||||||
// definition of t itself.
|
// definition of t itself.
|
||||||
//
|
//
|
||||||
// Templates can be redefined in successive calls to Parse,
|
// 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
|
// A template definition with a body containing only white space and comments
|
||||||
// is considered empty and will not replace an existing template's body.
|
// is considered empty and will not replace an existing template's body.
|
||||||
// This allows using Parse to add new named template definitions without
|
// 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
|
// Clone returns a duplicate of the template, including all associated
|
||||||
// templates. The actual representation is not copied, but the name space of
|
// 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
|
// 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
|
// common templates and use them with variant definitions for other templates
|
||||||
// by adding the variants after the clone is made.
|
// 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
|
// 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
|
// definitions will inherit the settings. An empty delimiter stands for the
|
||||||
// corresponding default: {{ or }}.
|
// corresponding default: {{ or }}.
|
||||||
// The return value is the template, so calls can be chained.
|
// 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]
|
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
|
// and panics if the error is non-nil. It is intended for use in variable initializations
|
||||||
// such as
|
// such as
|
||||||
//
|
//
|
||||||
|
@ -371,10 +371,10 @@ func Must(t *Template, err error) *Template {
|
||||||
return t
|
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
|
// 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.
|
// (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,
|
// When parsing multiple files with the same name in different directories,
|
||||||
// the last one mentioned will be the one that results.
|
// 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
|
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
|
// 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.
|
// 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
|
// The returned template will have the (base) name and (parsed) contents of the
|
||||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
// 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,
|
// When parsing multiple files with the same name in different directories,
|
||||||
// the last one mentioned will be the one that results.
|
// 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)
|
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.
|
// instead of the host operating system's file system.
|
||||||
// It accepts a list of glob patterns.
|
// It accepts a list of glob patterns.
|
||||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
// (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)
|
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.
|
// instead of the host operating system's file system.
|
||||||
// It accepts a list of glob patterns.
|
// It accepts a list of glob patterns.
|
||||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||||
|
|
|
@ -414,7 +414,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
|
||||||
// If "</script" appears in a regex literal, the '/' should not
|
// If "</script" appears in a regex literal, the '/' should not
|
||||||
// close the regex literal, and it will later be escaped to
|
// close the regex literal, and it will later be escaped to
|
||||||
// "\x3C/script" in escapeText.
|
// "\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++
|
i++
|
||||||
} else if !inCharset {
|
} else if !inCharset {
|
||||||
c.state, c.jsCtx = stateJS, jsCtxDivOp
|
c.state, c.jsCtx = stateJS, jsCtxDivOp
|
||||||
|
|
|
@ -132,15 +132,13 @@ func findGOROOT() (string, error) {
|
||||||
// If runtime.GOROOT() is non-empty, assume that it is valid.
|
// If runtime.GOROOT() is non-empty, assume that it is valid.
|
||||||
//
|
//
|
||||||
// (It might not be: for example, the user may have explicitly set GOROOT
|
// (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
|
// to the wrong directory. But this case is
|
||||||
// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
|
|
||||||
// rare, and if that happens the user can fix what they broke.)
|
// rare, and if that happens the user can fix what they broke.)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
|
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
|
||||||
// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
|
// binary was built with -trimpath).
|
||||||
// without GOROOT and the tree hasn't been moved there yet).
|
|
||||||
//
|
//
|
||||||
// Since this is internal/testenv, we can cheat and assume that the caller
|
// 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'
|
// 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
|
// MustHaveBuildMode reports whether the current system can build programs in
|
||||||
// the given build mode.
|
// the given build mode.
|
||||||
// If not, MustHaveBuildMode calls t.Skip with an explanation.
|
// If not, MustHaveBuildMode calls t.Skip with an explanation.
|
||||||
// Modified by Hugo (not needed)
|
// Modified by Hugo (not needed)
|
||||||
func MustHaveBuildMode(t testing.TB, buildmode string) {
|
func MustHaveBuildMode(t testing.TB, buildmode string) {
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasSymlink reports whether the current system can use os.Symlink.
|
// 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 {
|
func SyscallIsNotSupported(err error) bool {
|
||||||
return syscallIsNotSupported(err)
|
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) {
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
is executed; otherwise, dot is set to the value of the pipeline
|
||||||
and T1 is executed.
|
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
|
Arguments
|
||||||
|
|
||||||
An argument is a simple value, denoted by one of the following.
|
An argument is a simple value, denoted by one of the following.
|
||||||
|
|
|
@ -35,7 +35,7 @@ Josie
|
||||||
Name, Gift string
|
Name, Gift string
|
||||||
Attended bool
|
Attended bool
|
||||||
}
|
}
|
||||||
var recipients = []Recipient{
|
recipients := []Recipient{
|
||||||
{"Aunt Mildred", "bone china tea set", true},
|
{"Aunt Mildred", "bone china tea set", true},
|
||||||
{"Uncle John", "moleskin pants", false},
|
{"Uncle John", "moleskin pants", false},
|
||||||
{"Cousin Rodney", "", false},
|
{"Cousin Rodney", "", false},
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build go1.13
|
|
||||||
// +build go1.13
|
|
||||||
|
|
||||||
package template_test
|
package template_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -7,13 +7,12 @@ package template
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||||
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"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
|
// maxExecDepth specifies the maximum stack depth of templates within
|
||||||
|
@ -95,7 +94,7 @@ type missingValType struct{}
|
||||||
|
|
||||||
var missingVal = reflect.ValueOf(missingValType{})
|
var missingVal = reflect.ValueOf(missingValType{})
|
||||||
|
|
||||||
var missingValReflectType = reflect.TypeOf(missingValType{})
|
var missingValReflectType = reflect.TypeFor[missingValType]()
|
||||||
|
|
||||||
func isMissing(v reflect.Value) bool {
|
func isMissing(v reflect.Value) bool {
|
||||||
return v.IsValid() && v.Type() == missingValReflectType
|
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
|
// A template may be executed safely in parallel, although if parallel
|
||||||
// executions share a Writer the output may be interleaved.
|
// executions share a Writer the output may be interleaved.
|
||||||
//
|
//
|
||||||
// If data is a reflect.Value, the template applies to the concrete
|
// If data is a [reflect.Value], the template applies to the concrete
|
||||||
// value that the reflect.Value holds, as in fmt.Print.
|
// value that the reflect.Value holds, as in [fmt.Print].
|
||||||
func (t *Template) Execute(wr io.Writer, data any) error {
|
func (t *Template) Execute(wr io.Writer, data any) error {
|
||||||
return t.execute(wr, data)
|
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,
|
// DefinedTemplates returns a string listing the defined templates,
|
||||||
// prefixed by the string "; defined templates are: ". If there are none,
|
// prefixed by the string "; defined templates are: ". If there are none,
|
||||||
// it returns the empty string. For generating an error message here
|
// it returns the empty string. For generating an error message here
|
||||||
// and in html/template.
|
// and in [html/template].
|
||||||
func (t *Template) DefinedTemplates() string {
|
func (t *Template) DefinedTemplates() string {
|
||||||
if t.common == nil {
|
if t.common == nil {
|
||||||
return ""
|
return ""
|
||||||
|
@ -409,8 +408,8 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
om := fmtsort.Sort(val)
|
om := fmtsort.Sort(val)
|
||||||
for i, key := range om.Key {
|
for _, m := range om {
|
||||||
oneIteration(key, om.Value[i])
|
oneIteration(m.Key, m.Value)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case reflect.Chan:
|
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.
|
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 the object has type interface{}, dig down one level to the thing inside.
|
||||||
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
||||||
value = reflect.ValueOf(value.Interface()) // lovely!
|
value = value.Elem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, variable := range pipe.Decl {
|
for _, variable := range pipe.Decl {
|
||||||
|
@ -709,9 +708,9 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
errorType = reflect.TypeFor[error]()
|
||||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
fmtStringerType = reflect.TypeFor[fmt.Stringer]()
|
||||||
reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
|
reflectValueType = reflect.TypeFor[reflect.Value]()
|
||||||
)
|
)
|
||||||
|
|
||||||
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
// 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() {
|
} else if numIn != typ.NumIn() {
|
||||||
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
||||||
}
|
}
|
||||||
if !goodFunc(typ) {
|
if err := goodFunc(name, typ); err != nil {
|
||||||
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
s.errorf("%v", err)
|
||||||
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unwrap := func(v reflect.Value) reflect.Value {
|
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)
|
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)
|
v, err := safeCall(fun, argv)
|
||||||
// If we have an error that is not nil, stop execution and return that
|
// If we have an error that is not nil, stop execution and return that
|
||||||
// error to the caller.
|
// error to the caller.
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package template
|
package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -81,6 +84,9 @@ type T struct {
|
||||||
NilOKFunc func(*int) bool
|
NilOKFunc func(*int) bool
|
||||||
ErrFunc func() (string, error)
|
ErrFunc func() (string, error)
|
||||||
PanicFunc func() string
|
PanicFunc func() string
|
||||||
|
TooFewReturnCountFunc func()
|
||||||
|
TooManyReturnCountFunc func() (string, error, int)
|
||||||
|
InvalidReturnTypeFunc func() (string, bool)
|
||||||
// Template to test evaluation of templates.
|
// Template to test evaluation of templates.
|
||||||
Tmpl *Template
|
Tmpl *Template
|
||||||
// Unexported field; cannot be accessed by template.
|
// Unexported field; cannot be accessed by template.
|
||||||
|
@ -168,6 +174,9 @@ var tVal = &T{
|
||||||
NilOKFunc: func(s *int) bool { return s == nil },
|
NilOKFunc: func(s *int) bool { return s == nil },
|
||||||
ErrFunc: func() (string, error) { return "bla", nil },
|
ErrFunc: func() (string, error) { return "bla", nil },
|
||||||
PanicFunc: func() string { panic("test panic") },
|
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
|
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.
|
// of the max int boundary.
|
||||||
// We do it this way so the test doesn't depend on ints being 32 bits.
|
// We do it this way so the test doesn't depend on ints being 32 bits.
|
||||||
var (
|
var (
|
||||||
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
|
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
|
||||||
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
|
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
|
||||||
)
|
)
|
||||||
|
|
||||||
var execTests = []execTest{
|
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 $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 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 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.
|
||||||
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
|
{"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.
|
// Issue 31810. Check that a parenthesized first argument behaves properly.
|
||||||
func TestIssue31810(t *testing.T) {
|
func TestIssue31810(t *testing.T) {
|
||||||
// A simple value with no arguments is fine.
|
// A simple value with no arguments is fine.
|
||||||
|
|
|
@ -22,14 +22,14 @@ import (
|
||||||
// return value evaluates to non-nil during execution, execution terminates and
|
// return value evaluates to non-nil during execution, execution terminates and
|
||||||
// Execute returns that error.
|
// 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.
|
// unwrap them.
|
||||||
//
|
//
|
||||||
// When template execution invokes a function with an argument list, that list
|
// When template execution invokes a function with an argument list, that list
|
||||||
// must be assignable to the function's parameter types. Functions meant to
|
// 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
|
// 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
|
// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
|
||||||
// type can return interface{} or reflect.Value.
|
// type can return interface{} or [reflect.Value].
|
||||||
type FuncMap map[string]any
|
type FuncMap map[string]any
|
||||||
|
|
||||||
// builtins returns the FuncMap.
|
// builtins returns the FuncMap.
|
||||||
|
@ -39,7 +39,7 @@ type FuncMap map[string]any
|
||||||
func builtins() FuncMap {
|
func builtins() FuncMap {
|
||||||
return FuncMap{
|
return FuncMap{
|
||||||
"and": and,
|
"and": and,
|
||||||
"call": call,
|
"call": emptyCall,
|
||||||
"html": HTMLEscaper,
|
"html": HTMLEscaper,
|
||||||
"index": index,
|
"index": index,
|
||||||
"slice": slice,
|
"slice": slice,
|
||||||
|
@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
|
||||||
if v.Kind() != reflect.Func {
|
if v.Kind() != reflect.Func {
|
||||||
panic("value for " + name + " not a function")
|
panic("value for " + name + " not a function")
|
||||||
}
|
}
|
||||||
if !goodFunc(v.Type()) {
|
if err := goodFunc(name, v.Type()); err != nil {
|
||||||
panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
|
panic(err)
|
||||||
}
|
}
|
||||||
out[name] = v
|
out[name] = v
|
||||||
}
|
}
|
||||||
|
@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// goodFunc reports whether the function or method has the right result signature.
|
// 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.
|
// We allow functions with 1 result or 2 results where the second is an error.
|
||||||
switch {
|
switch numOut := typ.NumOut(); {
|
||||||
case typ.NumOut() == 1:
|
case numOut == 1:
|
||||||
return true
|
return nil
|
||||||
case typ.NumOut() == 2 && typ.Out(1) == errorType:
|
case numOut == 2 && typ.Out(1) == errorType:
|
||||||
return true
|
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.
|
// goodName reports whether the function name is a valid identifier.
|
||||||
|
@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
|
||||||
|
|
||||||
// Function invocation
|
// 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.
|
// call returns the result of evaluating the first argument as a function.
|
||||||
// The function must return 1 result, or 2 results, the second of which is an error.
|
// The function must return 1 result, or 2 results, the second of which is an error.
|
||||||
func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
||||||
fn = indirectInterface(fn)
|
fn = indirectInterface(fn)
|
||||||
if !fn.IsValid() {
|
if !fn.IsValid() {
|
||||||
return reflect.Value{}, fmt.Errorf("call of nil")
|
return reflect.Value{}, fmt.Errorf("call of nil")
|
||||||
}
|
}
|
||||||
typ := fn.Type()
|
typ := fn.Type()
|
||||||
if typ.Kind() != reflect.Func {
|
if typ.Kind() != reflect.Func {
|
||||||
return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
|
return reflect.Value{}, fmt.Errorf("non-function %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()
|
numIn := typ.NumIn()
|
||||||
var dddType reflect.Type
|
var dddType reflect.Type
|
||||||
if typ.IsVariadic() {
|
if typ.IsVariadic() {
|
||||||
if len(args) < numIn-1 {
|
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()
|
dddType = typ.In(numIn - 1).Elem()
|
||||||
} else {
|
} else {
|
||||||
if len(args) != numIn {
|
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))
|
argv := make([]reflect.Value, len(args))
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
|
|
||||||
// Functions and methods to parse templates.
|
// 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
|
// and panics if the error is non-nil. It is intended for use in variable
|
||||||
// initializations such as
|
// initializations such as
|
||||||
//
|
//
|
||||||
|
@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
|
||||||
return t
|
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
|
// 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.
|
// 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.
|
||||||
|
@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
|
||||||
// t. If an error occurs, parsing stops and the returned template is nil;
|
// t. If an error occurs, parsing stops and the returned template is nil;
|
||||||
// otherwise it is t. There must be at least one file.
|
// otherwise it is t. There must be at least one file.
|
||||||
// Since the templates created by ParseFiles are named by the base
|
// Since the templates created by ParseFiles are named by the base
|
||||||
// names of the argument files, t should usually have the name of one
|
// (see [filepath.Base]) names of the argument files, t should usually have the
|
||||||
// of the (base) names of the files. If it does not, depending on t's
|
// name of one of the (base) names of the files. If it does not, depending on
|
||||||
// contents before calling ParseFiles, t.Execute may fail. In that
|
// t's contents before calling ParseFiles, t.Execute may fail. In that
|
||||||
// case use t.ExecuteTemplate to execute a valid template.
|
// case use t.ExecuteTemplate to execute a valid template.
|
||||||
//
|
//
|
||||||
// When parsing multiple files with the same name in different directories,
|
// 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
|
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
|
// 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.
|
// 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
|
// The returned template will have the [filepath.Base] name and (parsed)
|
||||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
// contents of the first file matched by the pattern. ParseGlob is equivalent to
|
||||||
// ParseFiles with the list of files matched by the pattern.
|
// calling [ParseFiles] with the list of files matched by the pattern.
|
||||||
//
|
//
|
||||||
// When parsing multiple files with the same name in different directories,
|
// When parsing multiple files with the same name in different directories,
|
||||||
// the last one mentioned will be the one that results.
|
// 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
|
// ParseGlob parses the template definitions in the files identified by the
|
||||||
// pattern and associates the resulting templates with t. The files are matched
|
// 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
|
// 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
|
// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with
|
||||||
// list of files matched by the pattern.
|
// the list of files matched by the pattern.
|
||||||
//
|
//
|
||||||
// When parsing multiple files with the same name in different directories,
|
// When parsing multiple files with the same name in different directories,
|
||||||
// the last one mentioned will be the one that results.
|
// 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...)
|
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.
|
// 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.)
|
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||||
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
||||||
return parseFS(nil, fsys, patterns)
|
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.
|
// 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.)
|
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||||
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
||||||
t.init()
|
t.init()
|
||||||
|
|
|
@ -278,9 +278,8 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||||
} else if numIn != typ.NumIn() {
|
} else if numIn != typ.NumIn() {
|
||||||
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
||||||
}
|
}
|
||||||
if !goodFunc(typ) {
|
if err := goodFunc(name, typ); err != nil {
|
||||||
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
s.errorf("%v", err)
|
||||||
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unwrap := func(v reflect.Value) reflect.Value {
|
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)
|
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
|
// Added for Hugo
|
||||||
for i := 0; i < len(first); i++ {
|
for i := 0; i < len(first); i++ {
|
||||||
argv[i] = s.validateType(first[i], typ.In(i))
|
argv[i] = s.validateType(first[i], typ.In(i))
|
||||||
|
|
|
@ -2,18 +2,16 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build go1.13
|
|
||||||
// +build go1.13
|
|
||||||
|
|
||||||
package template_test
|
package template_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Issue 36021: verify that text/template doesn't prevent the linker from removing
|
// Issue 36021: verify that text/template doesn't prevent the linker from removing
|
||||||
|
@ -44,7 +42,7 @@ func main() {
|
||||||
`
|
`
|
||||||
td := t.TempDir()
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
|
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
|
||||||
|
|
|
@ -217,8 +217,12 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
|
||||||
}
|
}
|
||||||
v.writeTo(sb)
|
v.writeTo(sb)
|
||||||
}
|
}
|
||||||
|
if p.IsAssign {
|
||||||
|
sb.WriteString(" = ")
|
||||||
|
} else {
|
||||||
sb.WriteString(" := ")
|
sb.WriteString(" := ")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for i, c := range p.Cmds {
|
for i, c := range p.Cmds {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
sb.WriteString(" | ")
|
sb.WriteString(" | ")
|
||||||
|
@ -346,12 +350,12 @@ type IdentifierNode struct {
|
||||||
Ident string // The identifier's name.
|
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 {
|
func NewIdentifier(ident string) *IdentifierNode {
|
||||||
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
|
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.
|
// Chained for convenience.
|
||||||
// TODO: fix one day?
|
// TODO: fix one day?
|
||||||
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
||||||
|
@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
||||||
return i
|
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.
|
// Chained for convenience.
|
||||||
// TODO: fix one day?
|
// TODO: fix one day?
|
||||||
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
|
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
|
||||||
|
|
|
@ -42,7 +42,7 @@ const (
|
||||||
SkipFuncCheck // do not check that functions are defined
|
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 {
|
func (t *Tree) Copy() *Tree {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return 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
|
// 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
|
// given the specified name. If an error is encountered, parsing stops and an
|
||||||
// empty map is returned with the error.
|
// 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))
|
defer t.popVars(len(t.vars))
|
||||||
pipe = t.pipeline(context, itemRightDelim)
|
pipe = t.pipeline(context, itemRightDelim)
|
||||||
if context == "range" {
|
if context == "range" {
|
||||||
|
@ -535,28 +535,31 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
|
||||||
switch next.Type() {
|
switch next.Type() {
|
||||||
case nodeEnd: //done
|
case nodeEnd: //done
|
||||||
case nodeElse:
|
case nodeElse:
|
||||||
if allowElseIf {
|
// Special case for "else if" and "else with".
|
||||||
// Special case for "else if". If the "else" is followed immediately by an "if",
|
// If the "else" is followed immediately by an "if" or "with",
|
||||||
// the elseControl will have left the "if" token pending. Treat
|
// the elseControl will have left the "if" or "with" token pending. Treat
|
||||||
// {{if a}}_{{else if b}}_{{end}}
|
// {{if a}}_{{else if b}}_{{end}}
|
||||||
|
// {{with a}}_{{else with b}}_{{end}}
|
||||||
// as
|
// as
|
||||||
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
|
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}
|
||||||
// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
|
// {{with a}}_{{else}}{{with b}}_{{end}}{{end}}.
|
||||||
// is assumed. This technique works even for long if-else-if chains.
|
// To do this, parse the "if" or "with" as usual and stop at it {{end}};
|
||||||
// TODO: Should we allow else-if in with and range?
|
// the subsequent{{end}} is assumed. This technique works even for long if-else-if chains.
|
||||||
if t.peek().typ == itemIf {
|
if context == "if" && t.peek().typ == itemIf {
|
||||||
t.next() // Consume the "if" token.
|
t.next() // Consume the "if" token.
|
||||||
elseList = t.newList(next.Position())
|
elseList = t.newList(next.Position())
|
||||||
elseList.append(t.ifControl())
|
elseList.append(t.ifControl())
|
||||||
// Do not consume the next item - only one {{end}} required.
|
} else if context == "with" && t.peek().typ == itemWith {
|
||||||
break
|
t.next()
|
||||||
}
|
elseList = t.newList(next.Position())
|
||||||
}
|
elseList.append(t.withControl())
|
||||||
|
} else {
|
||||||
elseList, next = t.itemList()
|
elseList, next = t.itemList()
|
||||||
if next.Type() != nodeEnd {
|
if next.Type() != nodeEnd {
|
||||||
t.errorf("expected end; found %s", next)
|
t.errorf("expected end; found %s", next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return pipe.Position(), pipe.Line, pipe, list, elseList
|
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.
|
// If keyword is past.
|
||||||
func (t *Tree) ifControl() Node {
|
func (t *Tree) ifControl() Node {
|
||||||
return t.newIf(t.parseControl(true, "if"))
|
return t.newIf(t.parseControl("if"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range:
|
// Range:
|
||||||
|
@ -577,7 +580,7 @@ func (t *Tree) ifControl() Node {
|
||||||
//
|
//
|
||||||
// Range keyword is past.
|
// Range keyword is past.
|
||||||
func (t *Tree) rangeControl() Node {
|
func (t *Tree) rangeControl() Node {
|
||||||
r := t.newRange(t.parseControl(false, "range"))
|
r := t.newRange(t.parseControl("range"))
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,7 +591,7 @@ func (t *Tree) rangeControl() Node {
|
||||||
//
|
//
|
||||||
// If keyword is past.
|
// If keyword is past.
|
||||||
func (t *Tree) withControl() Node {
|
func (t *Tree) withControl() Node {
|
||||||
return t.newWith(t.parseControl(false, "with"))
|
return t.newWith(t.parseControl("with"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// End:
|
// End:
|
||||||
|
@ -606,10 +609,11 @@ func (t *Tree) endControl() Node {
|
||||||
//
|
//
|
||||||
// Else keyword is past.
|
// Else keyword is past.
|
||||||
func (t *Tree) elseControl() Node {
|
func (t *Tree) elseControl() Node {
|
||||||
// Special case for "else if".
|
|
||||||
peek := t.peekNonSpace()
|
peek := t.peekNonSpace()
|
||||||
if peek.typ == itemIf {
|
// The "{{else if ... " and "{{else with ..." will be
|
||||||
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
|
// 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)
|
return t.newElse(peek.pos, peek.line)
|
||||||
}
|
}
|
||||||
token := t.expect(itemRightDelim, "else")
|
token := t.expect(itemRightDelim, "else")
|
||||||
|
|
|
@ -33,9 +33,9 @@ var numberTests = []numberTest{
|
||||||
{"7_3", true, true, true, false, 73, 73, 73, 0},
|
{"7_3", true, true, true, false, 73, 73, 73, 0},
|
||||||
{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
|
{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||||
{"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},
|
{"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||||
{"0o73", true, true, true, false, 073, 073, 073, 0},
|
{"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||||
{"0O73", true, true, true, false, 073, 073, 073, 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},
|
||||||
{"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},
|
{"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},
|
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||||
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||||
// funny bases
|
// 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},
|
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||||
// character constants
|
// character constants
|
||||||
|
@ -176,74 +176,150 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var parseTests = []parseTest{
|
var parseTests = []parseTest{
|
||||||
{"empty", "", noError,
|
{
|
||||||
``},
|
"empty", "", noError,
|
||||||
{"comment", "{{/*\n\n\n*/}}", noError,
|
``,
|
||||||
``},
|
},
|
||||||
{"spaces", " \t\n", noError,
|
{
|
||||||
`" \t\n"`},
|
"comment", "{{/*\n\n\n*/}}", noError,
|
||||||
{"text", "some text", noError,
|
``,
|
||||||
`"some text"`},
|
},
|
||||||
{"emptyAction", "{{}}", hasError,
|
{
|
||||||
`{{}}`},
|
"spaces", " \t\n", noError,
|
||||||
{"field", "{{.X}}", noError,
|
`" \t\n"`,
|
||||||
`{{.X}}`},
|
},
|
||||||
{"simple command", "{{printf}}", noError,
|
{
|
||||||
`{{printf}}`},
|
"text", "some text", noError,
|
||||||
{"$ invocation", "{{$}}", noError,
|
`"some text"`,
|
||||||
"{{$}}"},
|
},
|
||||||
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
{
|
||||||
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
"emptyAction", "{{}}", hasError,
|
||||||
{"variable with fields", "{{$.I}}", noError,
|
`{{}}`,
|
||||||
"{{$.I}}"},
|
},
|
||||||
{"multi-word command", "{{printf `%d` 23}}", noError,
|
{
|
||||||
"{{printf `%d` 23}}"},
|
"field", "{{.X}}", noError,
|
||||||
{"pipeline", "{{.X|.Y}}", noError,
|
`{{.X}}`,
|
||||||
`{{.X | .Y}}`},
|
},
|
||||||
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
{
|
||||||
`{{$x := .X | .Y}}`},
|
"simple command", "{{printf}}", noError,
|
||||||
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
`{{printf}}`,
|
||||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
},
|
||||||
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
{
|
||||||
`{{(.Y .Z).Field}}`},
|
"$ invocation", "{{$}}", noError,
|
||||||
{"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}}`},
|
"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||||
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
"{{with $x := 3}}{{$x 23}}{{end}}",
|
||||||
`{{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}}"+"`},
|
"variable with fields", "{{$.I}}", noError,
|
||||||
{"simple range", "{{range .X}}hello{{end}}", noError,
|
"{{$.I}}",
|
||||||
`{{range .X}}"hello"{{end}}`},
|
},
|
||||||
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
{
|
||||||
`{{range .X.Y.Z}}"hello"{{end}}`},
|
"multi-word command", "{{printf `%d` 23}}", noError,
|
||||||
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
"{{printf `%d` 23}}",
|
||||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
},
|
||||||
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
{
|
||||||
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
"pipeline", "{{.X|.Y}}", noError,
|
||||||
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
`{{.X | .Y}}`,
|
||||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
},
|
||||||
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
{
|
||||||
`{{range .SI}}{{.}}{{end}}`},
|
"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||||
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
`{{$x := .X | .Y}}`,
|
||||||
`{{range $x := .SI}}{{.}}{{end}}`},
|
},
|
||||||
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
{
|
||||||
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||||
{"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
|
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
|
||||||
`{{range .SI}}{{.}}{{break}}{{end}}`},
|
},
|
||||||
{"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
|
{
|
||||||
`{{range .SI}}{{.}}{{continue}}{{end}}`},
|
"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||||
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
`{{(.Y .Z).Field}}`,
|
||||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
},
|
||||||
{"template", "{{template `x`}}", noError,
|
{
|
||||||
`{{template "x"}}`},
|
"simple if", "{{if .X}}hello{{end}}", noError,
|
||||||
{"template with arg", "{{template `x` .Y}}", noError,
|
`{{if .X}}"hello"{{end}}`,
|
||||||
`{{template "x" .Y}}`},
|
},
|
||||||
{"with", "{{with .X}}hello{{end}}", noError,
|
{
|
||||||
`{{with .X}}"hello"{{end}}`},
|
"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||||
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
`{{if .X}}"true"{{else}}"false"{{end}}`,
|
||||||
`{{with .X}}"hello"{{else}}"goodbye"{{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.
|
// Trimming spaces.
|
||||||
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
|
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
|
||||||
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
|
{"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 left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
|
||||||
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
||||||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""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 assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
|
||||||
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
|
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
|
||||||
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
|
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
|
||||||
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
|
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
|
||||||
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
|
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
|
||||||
{"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
|
{
|
||||||
`{{range .SI}}{{.}}{{continue}}{{end}}`},
|
"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
|
||||||
{"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
|
`{{range .SI}}{{.}}{{continue}}{{end}}`,
|
||||||
`{{range .SI}}{{.}}{{break}}{{end}}`},
|
},
|
||||||
|
{
|
||||||
|
"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
|
||||||
|
`{{range .SI}}{{.}}{{break}}{{end}}`,
|
||||||
|
},
|
||||||
|
|
||||||
// Errors.
|
// Errors.
|
||||||
{"unclosed action", "hello{{range", hasError, ""},
|
{"unclosed action", "hello{{range", hasError, ""},
|
||||||
|
@ -302,6 +384,9 @@ var parseTests = []parseTest{
|
||||||
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
||||||
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
|
{"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.
|
{"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 following a literal value
|
||||||
{"dot after integer", "{{1.E}}", hasError, ""},
|
{"dot after integer", "{{1.E}}", hasError, ""},
|
||||||
{"dot after float", "{{0.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
|
// 'break' is a defined function, don't treat it as a keyword: it should
|
||||||
// accept an argument successfully.
|
// accept an argument successfully.
|
||||||
var funcsWithKeywordFunc = map[string]any{
|
funcsWithKeywordFunc := map[string]any{
|
||||||
"break": func(in any) any { return in },
|
"break": func(in any) any { return in },
|
||||||
}
|
}
|
||||||
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
|
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.
|
// All failures, and the result is a string that must appear in the error message.
|
||||||
var errorTests = []parseTest{
|
var errorTests = []parseTest{
|
||||||
// Check line numbers are accurate.
|
// Check line numbers are accurate.
|
||||||
{"unclosed1",
|
{
|
||||||
|
"unclosed1",
|
||||||
"line1\n{{",
|
"line1\n{{",
|
||||||
hasError, `unclosed1:2: unclosed action`},
|
hasError, `unclosed1:2: unclosed action`,
|
||||||
{"unclosed2",
|
},
|
||||||
|
{
|
||||||
|
"unclosed2",
|
||||||
"line1\n{{define `x`}}line2\n{{",
|
"line1\n{{define `x`}}line2\n{{",
|
||||||
hasError, `unclosed2:3: unclosed action`},
|
hasError, `unclosed2:3: unclosed action`,
|
||||||
{"unclosed3",
|
},
|
||||||
|
{
|
||||||
|
"unclosed3",
|
||||||
"line1\n{{\"x\"\n\"y\"\n",
|
"line1\n{{\"x\"\n\"y\"\n",
|
||||||
hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
|
hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
|
||||||
{"unclosed4",
|
},
|
||||||
|
{
|
||||||
|
"unclosed4",
|
||||||
"{{\n\n\n\n\n",
|
"{{\n\n\n\n\n",
|
||||||
hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
|
hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
|
||||||
{"var1",
|
},
|
||||||
|
{
|
||||||
|
"var1",
|
||||||
"line1\n{{\nx\n}}",
|
"line1\n{{\nx\n}}",
|
||||||
hasError, `var1:3: function "x" not defined`},
|
hasError, `var1:3: function "x" not defined`,
|
||||||
|
},
|
||||||
// Specific errors.
|
// Specific errors.
|
||||||
{"function",
|
{
|
||||||
|
"function",
|
||||||
"{{foo}}",
|
"{{foo}}",
|
||||||
hasError, `function "foo" not defined`},
|
hasError, `function "foo" not defined`,
|
||||||
{"comment1",
|
},
|
||||||
|
{
|
||||||
|
"comment1",
|
||||||
"{{/*}}",
|
"{{/*}}",
|
||||||
hasError, `comment1:1: unclosed comment`},
|
hasError, `comment1:1: unclosed comment`,
|
||||||
{"comment2",
|
},
|
||||||
|
{
|
||||||
|
"comment2",
|
||||||
"{{/*\nhello\n}}",
|
"{{/*\nhello\n}}",
|
||||||
hasError, `comment2:1: unclosed comment`},
|
hasError, `comment2:1: unclosed comment`,
|
||||||
{"lparen",
|
},
|
||||||
|
{
|
||||||
|
"lparen",
|
||||||
"{{.X (1 2 3}}",
|
"{{.X (1 2 3}}",
|
||||||
hasError, `unclosed left paren`},
|
hasError, `unclosed left paren`,
|
||||||
{"rparen",
|
},
|
||||||
|
{
|
||||||
|
"rparen",
|
||||||
"{{.X 1 2 3 ) }}",
|
"{{.X 1 2 3 ) }}",
|
||||||
hasError, "unexpected right paren"},
|
hasError, "unexpected right paren",
|
||||||
{"rparen2",
|
},
|
||||||
|
{
|
||||||
|
"rparen2",
|
||||||
"{{(.X 1 2 3",
|
"{{(.X 1 2 3",
|
||||||
hasError, `unclosed action`},
|
hasError, `unclosed action`,
|
||||||
{"space",
|
},
|
||||||
|
{
|
||||||
|
"space",
|
||||||
"{{`x`3}}",
|
"{{`x`3}}",
|
||||||
hasError, `in operand`},
|
hasError, `in operand`,
|
||||||
{"idchar",
|
},
|
||||||
|
{
|
||||||
|
"idchar",
|
||||||
"{{a#}}",
|
"{{a#}}",
|
||||||
hasError, `'#'`},
|
hasError, `'#'`,
|
||||||
{"charconst",
|
},
|
||||||
|
{
|
||||||
|
"charconst",
|
||||||
"{{'a}}",
|
"{{'a}}",
|
||||||
hasError, `unterminated character constant`},
|
hasError, `unterminated character constant`,
|
||||||
{"stringconst",
|
},
|
||||||
|
{
|
||||||
|
"stringconst",
|
||||||
`{{"a}}`,
|
`{{"a}}`,
|
||||||
hasError, `unterminated quoted string`},
|
hasError, `unterminated quoted string`,
|
||||||
{"rawstringconst",
|
},
|
||||||
|
{
|
||||||
|
"rawstringconst",
|
||||||
"{{`a}}",
|
"{{`a}}",
|
||||||
hasError, `unterminated raw quoted string`},
|
hasError, `unterminated raw quoted string`,
|
||||||
{"number",
|
},
|
||||||
|
{
|
||||||
|
"number",
|
||||||
"{{0xi}}",
|
"{{0xi}}",
|
||||||
hasError, `number syntax`},
|
hasError, `number syntax`,
|
||||||
{"multidefine",
|
},
|
||||||
|
{
|
||||||
|
"multidefine",
|
||||||
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||||
hasError, `multiple definition of template`},
|
hasError, `multiple definition of template`,
|
||||||
{"eof",
|
},
|
||||||
|
{
|
||||||
|
"eof",
|
||||||
"{{range .X}}",
|
"{{range .X}}",
|
||||||
hasError, `unexpected EOF`},
|
hasError, `unexpected EOF`,
|
||||||
{"variable",
|
},
|
||||||
|
{
|
||||||
|
"variable",
|
||||||
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
// 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}}",
|
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||||
hasError, `unexpected ":="`},
|
hasError, `unexpected ":="`,
|
||||||
{"multidecl",
|
},
|
||||||
|
{
|
||||||
|
"multidecl",
|
||||||
"{{$a,$b,$c := 23}}",
|
"{{$a,$b,$c := 23}}",
|
||||||
hasError, `too many declarations`},
|
hasError, `too many declarations`,
|
||||||
{"undefvar",
|
},
|
||||||
|
{
|
||||||
|
"undefvar",
|
||||||
"{{$a}}",
|
"{{$a}}",
|
||||||
hasError, `undefined variable`},
|
hasError, `undefined variable`,
|
||||||
{"wrongdot",
|
},
|
||||||
|
{
|
||||||
|
"wrongdot",
|
||||||
"{{true.any}}",
|
"{{true.any}}",
|
||||||
hasError, `unexpected . after term`},
|
hasError, `unexpected . after term`,
|
||||||
{"wrongpipeline",
|
},
|
||||||
|
{
|
||||||
|
"wrongpipeline",
|
||||||
"{{12|false}}",
|
"{{12|false}}",
|
||||||
hasError, `non executable command in pipeline`},
|
hasError, `non executable command in pipeline`,
|
||||||
{"emptypipeline",
|
},
|
||||||
|
{
|
||||||
|
"emptypipeline",
|
||||||
`{{ ( ) }}`,
|
`{{ ( ) }}`,
|
||||||
hasError, `missing value for parenthesized pipeline`},
|
hasError, `missing value for parenthesized pipeline`,
|
||||||
{"multilinerawstring",
|
},
|
||||||
|
{
|
||||||
|
"multilinerawstring",
|
||||||
"{{ $v := `\n` }} {{",
|
"{{ $v := `\n` }} {{",
|
||||||
hasError, `multilinerawstring:2: unclosed action`},
|
hasError, `multilinerawstring:2: unclosed action`,
|
||||||
{"rangeundefvar",
|
},
|
||||||
|
{
|
||||||
|
"rangeundefvar",
|
||||||
"{{range $k}}{{end}}",
|
"{{range $k}}{{end}}",
|
||||||
hasError, `undefined variable`},
|
hasError, `undefined variable`,
|
||||||
{"rangeundefvars",
|
},
|
||||||
|
{
|
||||||
|
"rangeundefvars",
|
||||||
"{{range $k, $v}}{{end}}",
|
"{{range $k, $v}}{{end}}",
|
||||||
hasError, `undefined variable`},
|
hasError, `undefined variable`,
|
||||||
{"rangemissingvalue1",
|
},
|
||||||
|
{
|
||||||
|
"rangemissingvalue1",
|
||||||
"{{range $k,}}{{end}}",
|
"{{range $k,}}{{end}}",
|
||||||
hasError, `missing value for range`},
|
hasError, `missing value for range`,
|
||||||
{"rangemissingvalue2",
|
},
|
||||||
|
{
|
||||||
|
"rangemissingvalue2",
|
||||||
"{{range $k, $v := }}{{end}}",
|
"{{range $k, $v := }}{{end}}",
|
||||||
hasError, `missing value for range`},
|
hasError, `missing value for range`,
|
||||||
{"rangenotvariable1",
|
},
|
||||||
|
{
|
||||||
|
"rangenotvariable1",
|
||||||
"{{range $k, .}}{{end}}",
|
"{{range $k, .}}{{end}}",
|
||||||
hasError, `range can only initialize variables`},
|
hasError, `range can only initialize variables`,
|
||||||
{"rangenotvariable2",
|
},
|
||||||
|
{
|
||||||
|
"rangenotvariable2",
|
||||||
"{{range $k, 123 := .}}{{end}}",
|
"{{range $k, 123 := .}}{{end}}",
|
||||||
hasError, `range can only initialize variables`},
|
hasError, `range can only initialize variables`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrors(t *testing.T) {
|
func TestErrors(t *testing.T) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ type common struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Template is the representation of a parsed template. The *parse.Tree
|
// 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.
|
// as unexported by all other clients.
|
||||||
type Template struct {
|
type Template struct {
|
||||||
name string
|
name string
|
||||||
|
@ -79,7 +79,7 @@ func (t *Template) init() {
|
||||||
|
|
||||||
// Clone returns a duplicate of the template, including all associated
|
// Clone returns a duplicate of the template, including all associated
|
||||||
// templates. The actual representation is not copied, but the name space of
|
// 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
|
// 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
|
// common templates and use them with variant definitions for other templates
|
||||||
// by adding the variants after the clone is made.
|
// 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
|
// 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
|
// definitions will inherit the settings. An empty delimiter stands for the
|
||||||
// corresponding default: {{ or }}.
|
// corresponding default: {{ or }}.
|
||||||
// The return value is the template, so calls can be chained.
|
// The return value is the template, so calls can be chained.
|
||||||
|
|
|
@ -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
|
// Issue 10495
|
||||||
func TestCommentsBeforeBlockDefinition(t *testing.T) {
|
func TestCommentsBeforeBlockDefinition(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
Loading…
Reference in a new issue