mirror of
https://github.com/gohugoio/hugo.git
synced 2025-09-01 22:42:45 +02:00
@@ -98,7 +98,8 @@ data, defined in detail in the corresponding sections that follow.
|
||||
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
|
||||
|
||||
{{range pipeline}} T1 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
The value of the pipeline must be an array, slice, map, iter.Seq,
|
||||
iter.Seq2, integer or channel.
|
||||
If the value of the pipeline has length zero, nothing is output;
|
||||
otherwise, dot is set to the successive elements of the array,
|
||||
slice, or map and T1 is executed. If the value is a map and the
|
||||
@@ -106,7 +107,8 @@ data, defined in detail in the corresponding sections that follow.
|
||||
visited in sorted key order.
|
||||
|
||||
{{range pipeline}} T1 {{else}} T0 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
The value of the pipeline must be an array, slice, map, iter.Seq,
|
||||
iter.Seq2, integer or channel.
|
||||
If the value of the pipeline has length zero, dot is unaffected and
|
||||
T0 is executed; otherwise, dot is set to the successive elements
|
||||
of the array, slice, or map and T1 is executed.
|
||||
@@ -162,37 +164,55 @@ An argument is a simple value, denoted by one of the following.
|
||||
the host machine's ints are 32 or 64 bits.
|
||||
- The keyword nil, representing an untyped Go nil.
|
||||
- The character '.' (period):
|
||||
|
||||
.
|
||||
|
||||
The result is the value of dot.
|
||||
- A variable name, which is a (possibly empty) alphanumeric string
|
||||
preceded by a dollar sign, such as
|
||||
|
||||
$piOver2
|
||||
|
||||
or
|
||||
|
||||
$
|
||||
|
||||
The result is the value of the variable.
|
||||
Variables are described below.
|
||||
- The name of a field of the data, which must be a struct, preceded
|
||||
by a period, such as
|
||||
|
||||
.Field
|
||||
|
||||
The result is the value of the field. Field invocations may be
|
||||
chained:
|
||||
|
||||
.Field1.Field2
|
||||
|
||||
Fields can also be evaluated on variables, including chaining:
|
||||
|
||||
$x.Field1.Field2
|
||||
- The name of a key of the data, which must be a map, preceded
|
||||
by a period, such as
|
||||
|
||||
.Key
|
||||
|
||||
The result is the map element value indexed by the key.
|
||||
Key invocations may be chained and combined with fields to any
|
||||
depth:
|
||||
|
||||
.Field1.Key1.Field2.Key2
|
||||
|
||||
Although the key must be an alphanumeric identifier, unlike with
|
||||
field names they do not need to start with an upper case letter.
|
||||
Keys can also be evaluated on variables, including chaining:
|
||||
|
||||
$x.key1.key2
|
||||
- The name of a niladic method of the data, preceded by a period,
|
||||
such as
|
||||
|
||||
.Method
|
||||
|
||||
The result is the value of invoking the method with dot as the
|
||||
receiver, dot.Method(). Such a method must have one return value (of
|
||||
any type) or two return values, the second of which is an error.
|
||||
@@ -200,16 +220,22 @@ An argument is a simple value, denoted by one of the following.
|
||||
and an error is returned to the caller as the value of Execute.
|
||||
Method invocations may be chained and combined with fields and keys
|
||||
to any depth:
|
||||
|
||||
.Field1.Key1.Method1.Field2.Key2.Method2
|
||||
|
||||
Methods can also be evaluated on variables, including chaining:
|
||||
|
||||
$x.Method1.Field
|
||||
- The name of a niladic function, such as
|
||||
|
||||
fun
|
||||
|
||||
The result is the value of invoking the function, fun(). The return
|
||||
types and values behave as in methods. Functions and function
|
||||
names are described below.
|
||||
- A parenthesized instance of one the above, for grouping. The result
|
||||
may be accessed by a field or map key invocation.
|
||||
|
||||
print (.F1 arg1) (.F2 arg2)
|
||||
(.StructValuedMethod "arg").Field
|
||||
|
||||
|
@@ -35,7 +35,7 @@ Josie
|
||||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
recipients := []Recipient{
|
||||
var recipients = []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
|
@@ -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 go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
@@ -395,6 +395,22 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||
s.walk(elem, r.List)
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
s.errorf("can't use %v to iterate over more than one variable", val)
|
||||
break
|
||||
}
|
||||
run := false
|
||||
for v := range val.Seq() {
|
||||
run = true
|
||||
// Pass element as second value, as we do for channels.
|
||||
oneIteration(reflect.Value{}, v)
|
||||
}
|
||||
if !run {
|
||||
break
|
||||
}
|
||||
return
|
||||
case reflect.Array, reflect.Slice:
|
||||
if val.Len() == 0 {
|
||||
break
|
||||
@@ -434,6 +450,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||
return
|
||||
case reflect.Invalid:
|
||||
break // An invalid value is likely a nil map, etc. and acts like an empty map.
|
||||
case reflect.Func:
|
||||
if val.Type().CanSeq() {
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
s.errorf("can't use %v iterate over more than one variable", val)
|
||||
break
|
||||
}
|
||||
run := false
|
||||
for v := range val.Seq() {
|
||||
run = true
|
||||
// Pass element as second value,
|
||||
// as we do for channels.
|
||||
oneIteration(reflect.Value{}, v)
|
||||
}
|
||||
if !run {
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
if val.Type().CanSeq2() {
|
||||
run := false
|
||||
for i, v := range val.Seq2() {
|
||||
run = true
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
oneIteration(i, v)
|
||||
} else {
|
||||
// If there is only one range variable,
|
||||
// oneIteration will use the
|
||||
// second value.
|
||||
oneIteration(reflect.Value{}, i)
|
||||
}
|
||||
}
|
||||
if !run {
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
s.errorf("range can't iterate over %v", val)
|
||||
}
|
||||
@@ -757,7 +810,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
||||
return v
|
||||
}
|
||||
}
|
||||
if final != missingVal {
|
||||
if !final.Equal(missingVal) {
|
||||
// The last argument to and/or is coming from
|
||||
// the pipeline. We didn't short circuit on an earlier
|
||||
// argument, so we are going to return this one.
|
||||
@@ -803,7 +856,13 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
||||
// 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()
|
||||
var calleeName string
|
||||
if len(args) == 0 {
|
||||
// final must be present or we would have errored out above.
|
||||
calleeName = final.String()
|
||||
} else {
|
||||
calleeName = args[0].String()
|
||||
}
|
||||
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
|
||||
fun = reflect.ValueOf(call)
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -412,6 +413,9 @@ var execTests = []execTest{
|
||||
{"Interface Call", `{{stringer .S}}`, "foozle", map[string]any{"S": bytes.NewBufferString("foozle")}, true},
|
||||
{".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
|
||||
{"call nil", "{{call nil}}", "", tVal, false},
|
||||
{"empty call", "{{call}}", "", tVal, false},
|
||||
{"empty call after pipe valid", "{{.ErrFunc | call}}", "bla", tVal, true},
|
||||
{"empty call after pipe invalid", "{{1 | call}}", "", tVal, false},
|
||||
|
||||
// Erroneous function calls (check args).
|
||||
{".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false},
|
||||
@@ -618,6 +622,30 @@ var execTests = []execTest{
|
||||
{"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
|
||||
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
|
||||
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
|
||||
{"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
|
||||
{"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
|
||||
{"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
|
||||
{"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
|
||||
{"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
|
||||
{"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
|
||||
{"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
|
||||
{"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
|
||||
{"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
|
||||
{"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
|
||||
{"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},
|
||||
{"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true},
|
||||
{"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true},
|
||||
{"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true},
|
||||
{"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true},
|
||||
{"range int", rangeTestInt, rangeTestData[int](), int(5), true},
|
||||
{"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true},
|
||||
{"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true},
|
||||
{"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true},
|
||||
{"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true},
|
||||
{"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true},
|
||||
{"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true},
|
||||
{"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true},
|
||||
{"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true},
|
||||
|
||||
// Cute examples.
|
||||
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
|
||||
@@ -722,6 +750,37 @@ var execTests = []execTest{
|
||||
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
|
||||
}
|
||||
|
||||
func fVal1(i int) iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
for v := range i {
|
||||
if !yield(v) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fVal2(i int) iter.Seq2[int, int] {
|
||||
return func(yield func(int, int) bool) {
|
||||
for v := range i {
|
||||
if !yield(v, v+1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}`
|
||||
|
||||
func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string {
|
||||
I := T(5)
|
||||
var buf strings.Builder
|
||||
for i := T(0); i < I; i++ {
|
||||
fmt.Fprintf(&buf, "%T%d", i, i)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func zeroArgs() string {
|
||||
return "zeroArgs"
|
||||
}
|
||||
|
@@ -304,14 +304,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if args != nil {
|
||||
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
||||
}
|
||||
|
||||
typ := fun.Type()
|
||||
numFirst := len(first)
|
||||
numFirst := len(first) // Added for Hugo
|
||||
numIn := len(args) + numFirst // Added for Hugo
|
||||
if final != missingVal {
|
||||
if !isMissing(final) {
|
||||
numIn++
|
||||
}
|
||||
numFixed := len(args) + len(first) // Adjusted for Hugo
|
||||
@@ -346,7 +346,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
return v
|
||||
}
|
||||
}
|
||||
if final != missingVal {
|
||||
if !final.Equal(missingVal) {
|
||||
// The last argument to and/or is coming from
|
||||
// the pipeline. We didn't short circuit on an earlier
|
||||
// argument, so we are going to return this one.
|
||||
@@ -373,7 +373,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
}
|
||||
}
|
||||
// Add final value if necessary.
|
||||
if final != missingVal {
|
||||
if !isMissing(final) {
|
||||
t := typ.In(typ.NumIn() - 1)
|
||||
if typ.IsVariadic() {
|
||||
if numIn-1 < numFixed {
|
||||
@@ -392,7 +392,13 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
// 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()
|
||||
var calleeName string
|
||||
if len(args) == 0 {
|
||||
// final must be present or we would have errored out above.
|
||||
calleeName = final.String()
|
||||
} else {
|
||||
calleeName = args[0].String()
|
||||
}
|
||||
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
|
||||
fun = reflect.ValueOf(call)
|
||||
}
|
||||
|
@@ -2,16 +2,18 @@
|
||||
// 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
|
||||
@@ -42,7 +44,7 @@ func main() {
|
||||
`
|
||||
td := t.TempDir()
|
||||
|
||||
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
|
||||
|
@@ -2,8 +2,8 @@
|
||||
// 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
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
@@ -11,11 +11,10 @@ package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,32 +31,22 @@ type multiParseTest struct {
|
||||
}
|
||||
|
||||
var multiParseTests = []multiParseTest{
|
||||
{
|
||||
"empty", "", noError,
|
||||
{"empty", "", noError,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
nil},
|
||||
{"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
[]string{"foo"},
|
||||
[]string{" FOO "},
|
||||
},
|
||||
{
|
||||
"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{" FOO "}},
|
||||
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{"foo", "bar"},
|
||||
[]string{" FOO ", " BAR "},
|
||||
},
|
||||
[]string{" FOO ", " BAR "}},
|
||||
// errors
|
||||
{
|
||||
"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
{"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
{"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
nil},
|
||||
}
|
||||
|
||||
func TestMultiParse(t *testing.T) {
|
||||
@@ -443,7 +432,7 @@ func TestIssue19294(t *testing.T) {
|
||||
// by the contents of "stylesheet", but if the internal map associating
|
||||
// names with templates is built in the wrong order, the empty block
|
||||
// looks non-empty and this doesn't happen.
|
||||
inlined := map[string]string{
|
||||
var inlined = map[string]string{
|
||||
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
|
||||
"xhtml": `{{block "stylesheet" .}}{{end}}`,
|
||||
}
|
||||
|
@@ -352,6 +352,7 @@ func lexComment(l *lexer) stateFn {
|
||||
if !delim {
|
||||
return l.errorf("comment ends before closing delimiter")
|
||||
}
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
i := l.thisItem(itemComment)
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
|
@@ -548,6 +548,16 @@ var lexPosTests = []lexTest{
|
||||
{itemRightDelim, 11, "}}", 2},
|
||||
{itemEOF, 13, "", 2},
|
||||
}},
|
||||
{"longcomment", "{{/*\n*/}}\n{{undefinedFunction \"test\"}}", []item{
|
||||
{itemComment, 2, "/*\n*/", 1},
|
||||
{itemText, 9, "\n", 2},
|
||||
{itemLeftDelim, 10, "{{", 3},
|
||||
{itemIdentifier, 12, "undefinedFunction", 3},
|
||||
{itemSpace, 29, " ", 3},
|
||||
{itemString, 30, "\"test\"", 3},
|
||||
{itemRightDelim, 36, "}}", 3},
|
||||
{itemEOF, 38, "", 3},
|
||||
}},
|
||||
}
|
||||
|
||||
// The other tests don't check position, to make the test cases easier to construct.
|
||||
|
@@ -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 go1.13
|
||||
// +build go1.13
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
@@ -33,9 +36,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, 0o73, 0o73, 0o73, 0},
|
||||
{"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"0O73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0o73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0O73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
@@ -61,7 +64,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, 0o123, 0o123, 0o123, 0},
|
||||
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||
// character constants
|
||||
@@ -176,150 +179,78 @@ 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}}`,
|
||||
},
|
||||
{
|
||||
"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}}`,
|
||||
},
|
||||
{"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"`},
|
||||
@@ -328,24 +259,18 @@ 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, ""},
|
||||
@@ -487,7 +412,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
|
||||
{
|
||||
// 'break' is a defined function, don't treat it as a keyword: it should
|
||||
// accept an argument successfully.
|
||||
funcsWithKeywordFunc := map[string]any{
|
||||
var funcsWithKeywordFunc = map[string]any{
|
||||
"break": func(in any) any { return in },
|
||||
}
|
||||
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
|
||||
@@ -574,168 +499,104 @@ 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) {
|
||||
|
@@ -6,6 +6,7 @@ package template
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"maps"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
@@ -102,12 +103,8 @@ func (t *Template) Clone() (*Template, error) {
|
||||
}
|
||||
t.muFuncs.RLock()
|
||||
defer t.muFuncs.RUnlock()
|
||||
for k, v := range t.parseFuncs {
|
||||
nt.parseFuncs[k] = v
|
||||
}
|
||||
for k, v := range t.execFuncs {
|
||||
nt.execFuncs[k] = v
|
||||
}
|
||||
maps.Copy(nt.parseFuncs, t.parseFuncs)
|
||||
maps.Copy(nt.execFuncs, t.execFuncs)
|
||||
return nt, nil
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user