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