Fix Go template script escaping

Fixes #6695
This commit is contained in:
Bjørn Erik Pedersen
2020-05-23 15:32:27 +02:00
parent c34bf48560
commit 6c3c6686f5
27 changed files with 1090 additions and 292 deletions

View File

@@ -102,8 +102,8 @@ data, defined in detail in the corresponding sections that follow.
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
keys are of basic type with a defined order ("comparable"), the
elements will be visited in sorted key order.
keys are of basic type with a defined order, the elements will be
visited in sorted key order.
{{range pipeline}} T1 {{else}} T0 {{end}}
The value of the pipeline must be an array, slice, map, or channel.
@@ -385,14 +385,12 @@ returning in effect
(Unlike with || in Go, however, eq is a function call and all the
arguments will be evaluated.)
The comparison functions work on basic types only (or named basic
types, such as "type Celsius float32"). They implement the Go rules
for comparison of values, except that size and exact type are
ignored, so any integer value, signed or unsigned, may be compared
with any other integer value. (The arithmetic value is compared,
not the bit pattern, so all negative integers are less than all
unsigned integers.) However, as usual, one may not compare an int
with a float32 and so on.
The comparison functions work on any values whose type Go defines as
comparable. For basic types such as integers, the rules are relaxed:
size and exact type are ignored, so any integer value, signed or unsigned,
may be compared with any other integer value. (The arithmetic value is compared,
not the bit pattern, so all negative integers are less than all unsigned integers.)
However, as usual, one may not compare an int with a float32 and so on.
Associated templates

View File

@@ -5,7 +5,6 @@
package template
import (
"bytes"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@@ -230,21 +229,19 @@ func (t *Template) DefinedTemplates() string {
if t.common == nil {
return ""
}
var b bytes.Buffer
var b strings.Builder
for name, tmpl := range t.tmpl {
if tmpl.Tree == nil || tmpl.Root == nil {
continue
}
if b.Len() > 0 {
if b.Len() == 0 {
b.WriteString("; defined templates are: ")
} else {
b.WriteString(", ")
}
fmt.Fprintf(&b, "%q", name)
}
var s string
if b.Len() > 0 {
s = "; defined templates are: " + b.String()
}
return s
return b.String()
}
// Walk functions step through the major pieces of the template structure,
@@ -464,7 +461,8 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
// Must be a function.
return s.evalFunction(dot, n, cmd, cmd.Args, final)
case *parse.PipeNode:
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
// Parenthesized pipeline. The arguments are all inside the pipeline; final must be absent.
s.notAFunction(cmd.Args, final)
return s.evalPipeline(dot, n)
case *parse.VariableNode:
return s.evalVariableNode(dot, n, cmd.Args, final)
@@ -499,20 +497,29 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
switch {
case constant.IsComplex:
return reflect.ValueOf(constant.Complex128) // incontrovertible.
case constant.IsFloat && !isHexInt(constant.Text) && strings.ContainsAny(constant.Text, ".eEpP"):
case constant.IsFloat &&
!isHexInt(constant.Text) && !isRuneInt(constant.Text) &&
strings.ContainsAny(constant.Text, ".eEpP"):
return reflect.ValueOf(constant.Float64)
case constant.IsInt:
n := int(constant.Int64)
if int64(n) != constant.Int64 {
s.errorf("%s overflows int", constant.Text)
}
return reflect.ValueOf(n)
case constant.IsUint:
s.errorf("%s overflows int", constant.Text)
}
return zero
}
func isRuneInt(s string) bool {
return len(s) > 0 && s[0] == '\''
}
func isHexInt(s string) bool {
return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP")
}

View File

@@ -354,6 +354,12 @@ var execTests = []execTest{
{"field on interface", "{{.foo}}", "<no value>", nil, true},
{"field on parenthesized interface", "{{(.).foo}}", "<no value>", nil, true},
// Issue 31810: Parenthesized first element of pipeline with arguments.
// See also TestIssue31810.
{"unparenthesized non-function", "{{1 2}}", "", nil, false},
{"parenthesized non-function", "{{(1) 2}}", "", nil, false},
{"parenthesized non-function with no args", "{{(1)}}", "1", nil, true}, // This is fine.
// Method calls.
{".Method0", "-{{.Method0}}-", "-M0-", tVal, true},
{".Method1(1234)", "-{{.Method1 1234}}-", "-1234-", tVal, true},
@@ -498,6 +504,7 @@ var execTests = []execTest{
{"map MUI64S", "{{index .MUI64S 3}}", "ui643", tVal, true},
{"map MI8S", "{{index .MI8S 3}}", "i83", tVal, true},
{"map MUI8S", "{{index .MUI8S 2}}", "u82", tVal, true},
{"index of an interface field", "{{index .Empty3 0}}", "7", tVal, true},
// Slicing.
{"slice[:]", "{{slice .SI}}", "[3 4 5]", tVal, true},
@@ -523,12 +530,14 @@ var execTests = []execTest{
{"string[1:2]", "{{slice .S 1 2}}", "y", tVal, true},
{"out of range", "{{slice .S 1 5}}", "", tVal, false},
{"3-index slice of string", "{{slice .S 1 2 2}}", "", tVal, false},
{"slice of an interface field", "{{slice .Empty3 0 1}}", "[7]", tVal, true},
// Len.
{"slice", "{{len .SI}}", "3", tVal, true},
{"map", "{{len .MSI }}", "3", tVal, true},
{"len of int", "{{len 3}}", "", tVal, false},
{"len of nothing", "{{len .Empty0}}", "", tVal, false},
{"len of an interface field", "{{len .Empty3}}", "2", tVal, true},
// With.
{"with true", "{{with true}}{{.}}{{end}}", "true", tVal, true},
@@ -665,6 +674,12 @@ var execTests = []execTest{
{"bug17c", "{{len .NonEmptyInterfacePtS}}", "2", tVal, true},
{"bug17d", "{{index .NonEmptyInterfacePtS 0}}", "a", tVal, true},
{"bug17e", "{{range .NonEmptyInterfacePtS}}-{{.}}-{{end}}", "-a--b-", tVal, true},
// More variadic function corner cases. Some runes would get evaluated
// as constant floats instead of ints. Issue 34483.
{"bug18a", "{{eq . '.'}}", "true", '.', true},
{"bug18b", "{{eq . 'e'}}", "true", 'e', true},
{"bug18c", "{{eq . 'P'}}", "true", 'P', true},
}
func zeroArgs() string {
@@ -898,7 +913,9 @@ func TestJSEscaping(t *testing.T) {
{`Go "jump" \`, `Go \"jump\" \\`},
{`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`},
{"unprintable \uFDFF", `unprintable \uFDFF`},
{`<html>`, `\x3Chtml\x3E`},
{`<html>`, `\u003Chtml\u003E`},
{`no = in attributes`, `no \u003D in attributes`},
{`&#x27; does not become HTML entity`, `\u0026#x27; does not become HTML entity`},
}
for _, tc := range testCases {
s := JSEscapeString(tc.in)
@@ -1158,19 +1175,41 @@ var cmpTests = []cmpTest{
{"ge .Uthree .NegOne", "true", true},
{"eq (index `x` 0) 'x'", "true", true}, // The example that triggered this rule.
{"eq (index `x` 0) 'y'", "false", true},
{"eq .V1 .V2", "true", true},
{"eq .Ptr .Ptr", "true", true},
{"eq .Ptr .NilPtr", "false", true},
{"eq .NilPtr .NilPtr", "true", true},
{"eq .Iface1 .Iface1", "true", true},
{"eq .Iface1 .Iface2", "false", true},
{"eq .Iface2 .Iface2", "true", true},
// Errors
{"eq `xy` 1", "", false}, // Different types.
{"eq 2 2.0", "", false}, // Different types.
{"lt true true", "", false}, // Unordered types.
{"lt 1+0i 1+0i", "", false}, // Unordered types.
{"eq `xy` 1", "", false}, // Different types.
{"eq 2 2.0", "", false}, // Different types.
{"lt true true", "", false}, // Unordered types.
{"lt 1+0i 1+0i", "", false}, // Unordered types.
{"eq .Ptr 1", "", false}, // Incompatible types.
{"eq .Ptr .NegOne", "", false}, // Incompatible types.
{"eq .Map .Map", "", false}, // Uncomparable types.
{"eq .Map .V1", "", false}, // Uncomparable types.
}
func TestComparison(t *testing.T) {
b := new(bytes.Buffer)
var cmpStruct = struct {
Uthree, Ufour uint
NegOne, Three int
}{3, 4, -1, 3}
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
Map map[int]int
V1, V2 V
Iface1, Iface2 fmt.Stringer
}{
Uthree: 3,
Ufour: 4,
NegOne: -1,
Three: 3,
Ptr: new(int),
Iface1: b,
}
for _, test := range cmpTests {
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
tmpl, err := New("empty").Parse(text)
@@ -1622,3 +1661,41 @@ func TestExecutePanicDuringCall(t *testing.T) {
}
}
}
// Issue 31810. Check that a parenthesized first argument behaves properly.
func TestIssue31810(t *testing.T) {
// A simple value with no arguments is fine.
var b bytes.Buffer
const text = "{{ (.) }}"
tmpl, err := New("").Parse(text)
if err != nil {
t.Error(err)
}
err = tmpl.Execute(&b, "result")
if err != nil {
t.Error(err)
}
if b.String() != "result" {
t.Errorf("%s got %q, expected %q", text, b.String(), "result")
}
// Even a plain function fails - need to use call.
f := func() string { return "result" }
b.Reset()
err = tmpl.Execute(&b, f)
if err == nil {
t.Error("expected error with no call, got none")
}
// Works if the function is explicitly called.
const textCall = "{{ (call .) }}"
tmpl, err = New("").Parse(textCall)
b.Reset()
err = tmpl.Execute(&b, f)
if err != nil {
t.Error(err)
}
if b.String() != "result" {
t.Errorf("%s got %q, expected %q", textCall, b.String(), "result")
}
}

View File

@@ -12,6 +12,7 @@ import (
"net/url"
"reflect"
"strings"
"sync"
"unicode"
"unicode/utf8"
)
@@ -29,31 +30,49 @@ import (
// type can return interface{} or reflect.Value.
type FuncMap map[string]interface{}
var builtins = FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"slice": slice,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
// builtins returns the FuncMap.
// It is not a global variable so the linker can dead code eliminate
// more when this isn't called. See golang.org/issue/36021.
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
func builtins() FuncMap {
return FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"slice": slice,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
// Comparisons
"eq": eq, // ==
"ge": ge, // >=
"gt": gt, // >
"le": le, // <=
"lt": lt, // <
"ne": ne, // !=
// Comparisons
"eq": eq, // ==
"ge": ge, // >=
"gt": gt, // >
"le": le, // <=
"lt": lt, // <
"ne": ne, // !=
}
}
var builtinFuncs = createValueFuncs(builtins)
var builtinFuncsOnce struct {
sync.Once
v map[string]reflect.Value
}
// builtinFuncsOnce lazily computes & caches the builtinFuncs map.
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
func builtinFuncs() map[string]reflect.Value {
builtinFuncsOnce.Do(func() {
builtinFuncsOnce.v = createValueFuncs(builtins())
})
return builtinFuncsOnce.v
}
// createValueFuncs turns a FuncMap into a map[string]reflect.Value
func createValueFuncs(funcMap FuncMap) map[string]reflect.Value {
@@ -125,7 +144,7 @@ func findFunction(name string, tmpl *Template) (reflect.Value, bool) {
return fn, true
}
}
if fn := builtinFuncs[name]; fn.IsValid() {
if fn := builtinFuncs()[name]; fn.IsValid() {
return fn, true
}
return reflect.Value{}, false
@@ -185,41 +204,41 @@ func indexArg(index reflect.Value, cap int) (int, error) {
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
v := indirectInterface(item)
if !v.IsValid() {
item = indirectInterface(item)
if !item.IsValid() {
return reflect.Value{}, fmt.Errorf("index of untyped nil")
}
for _, i := range indexes {
index := indirectInterface(i)
for _, index := range indexes {
index = indirectInterface(index)
var isNil bool
if v, isNil = indirect(v); isNil {
if item, isNil = indirect(item); isNil {
return reflect.Value{}, fmt.Errorf("index of nil pointer")
}
switch v.Kind() {
switch item.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
x, err := indexArg(index, v.Len())
x, err := indexArg(index, item.Len())
if err != nil {
return reflect.Value{}, err
}
v = v.Index(x)
item = item.Index(x)
case reflect.Map:
index, err := prepareArg(index, v.Type().Key())
index, err := prepareArg(index, item.Type().Key())
if err != nil {
return reflect.Value{}, err
}
if x := v.MapIndex(index); x.IsValid() {
v = x
if x := item.MapIndex(index); x.IsValid() {
item = x
} else {
v = reflect.Zero(v.Type().Elem())
item = reflect.Zero(item.Type().Elem())
}
case reflect.Invalid:
// the loop holds invariant: v.IsValid()
// the loop holds invariant: item.IsValid()
panic("unreachable")
default:
return reflect.Value{}, fmt.Errorf("can't index item of type %s", v.Type())
return reflect.Value{}, fmt.Errorf("can't index item of type %s", item.Type())
}
}
return v, nil
return item, nil
}
// Slicing.
@@ -229,29 +248,27 @@ func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error)
// is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first
// argument must be a string, slice, or array.
func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
var (
cap int
v = indirectInterface(item)
)
if !v.IsValid() {
item = indirectInterface(item)
if !item.IsValid() {
return reflect.Value{}, fmt.Errorf("slice of untyped nil")
}
if len(indexes) > 3 {
return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
}
switch v.Kind() {
var cap int
switch item.Kind() {
case reflect.String:
if len(indexes) == 3 {
return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string")
}
cap = v.Len()
cap = item.Len()
case reflect.Array, reflect.Slice:
cap = v.Cap()
cap = item.Cap()
default:
return reflect.Value{}, fmt.Errorf("can't slice item of type %s", v.Type())
return reflect.Value{}, fmt.Errorf("can't slice item of type %s", item.Type())
}
// set default values for cases item[:], item[i:].
idx := [3]int{0, v.Len()}
idx := [3]int{0, item.Len()}
for i, index := range indexes {
x, err := indexArg(index, cap)
if err != nil {
@@ -276,20 +293,16 @@ func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error)
// Length
// length returns the length of the item, with an error if it has no defined length.
func length(item interface{}) (int, error) {
v := reflect.ValueOf(item)
if !v.IsValid() {
return 0, fmt.Errorf("len of untyped nil")
}
v, isNil := indirect(v)
func length(item reflect.Value) (int, error) {
item, isNil := indirect(item)
if isNil {
return 0, fmt.Errorf("len of nil pointer")
}
switch v.Kind() {
switch item.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
return v.Len(), nil
return item.Len(), nil
}
return 0, fmt.Errorf("len of type %s", v.Type())
return 0, fmt.Errorf("len of type %s", item.Type())
}
// Function invocation
@@ -297,11 +310,11 @@ func length(item interface{}) (int, error) {
// 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) {
v := indirectInterface(fn)
if !v.IsValid() {
fn = indirectInterface(fn)
if !fn.IsValid() {
return reflect.Value{}, fmt.Errorf("call of nil")
}
typ := v.Type()
typ := fn.Type()
if typ.Kind() != reflect.Func {
return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
}
@@ -322,7 +335,7 @@ func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
}
argv := make([]reflect.Value, len(args))
for i, arg := range args {
value := indirectInterface(arg)
arg = indirectInterface(arg)
// Compute the expected type. Clumsy because of variadics.
argType := dddType
if !typ.IsVariadic() || i < numIn-1 {
@@ -330,11 +343,11 @@ func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
}
var err error
if argv[i], err = prepareArg(value, argType); err != nil {
if argv[i], err = prepareArg(arg, argType); err != nil {
return reflect.Value{}, fmt.Errorf("arg %d: %s", i, err)
}
}
return safeCall(v, argv)
return safeCall(fn, argv)
}
// safeCall runs fun.Call(args), and returns the resulting value and error, if
@@ -440,47 +453,53 @@ func basicKind(v reflect.Value) (kind, error) {
// eq evaluates the comparison a == b || a == c || ...
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
v1 := indirectInterface(arg1)
k1, err := basicKind(v1)
if err != nil {
return false, err
arg1 = indirectInterface(arg1)
if arg1 != zero {
if t1 := arg1.Type(); !t1.Comparable() {
return false, fmt.Errorf("uncomparable type %s: %v", t1, arg1)
}
}
if len(arg2) == 0 {
return false, errNoComparison
}
k1, _ := basicKind(arg1)
for _, arg := range arg2 {
v2 := indirectInterface(arg)
k2, err := basicKind(v2)
if err != nil {
return false, err
}
arg = indirectInterface(arg)
k2, _ := basicKind(arg)
truth := false
if k1 != k2 {
// Special case: Can compare integer values regardless of type's sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
truth = arg1.Int() >= 0 && uint64(arg1.Int()) == arg.Uint()
case k1 == uintKind && k2 == intKind:
truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
default:
return false, errBadComparison
}
} else {
switch k1 {
case boolKind:
truth = v1.Bool() == v2.Bool()
truth = arg1.Bool() == arg.Bool()
case complexKind:
truth = v1.Complex() == v2.Complex()
truth = arg1.Complex() == arg.Complex()
case floatKind:
truth = v1.Float() == v2.Float()
truth = arg1.Float() == arg.Float()
case intKind:
truth = v1.Int() == v2.Int()
truth = arg1.Int() == arg.Int()
case stringKind:
truth = v1.String() == v2.String()
truth = arg1.String() == arg.String()
case uintKind:
truth = v1.Uint() == v2.Uint()
truth = arg1.Uint() == arg.Uint()
default:
panic("invalid kind")
if arg == zero {
truth = arg1 == arg
} else {
if t2 := arg.Type(); !t2.Comparable() {
return false, fmt.Errorf("uncomparable type %s: %v", t2, arg)
}
truth = arg1.Interface() == arg.Interface()
}
}
}
if truth {
@@ -499,13 +518,13 @@ func ne(arg1, arg2 reflect.Value) (bool, error) {
// lt evaluates the comparison a < b.
func lt(arg1, arg2 reflect.Value) (bool, error) {
v1 := indirectInterface(arg1)
k1, err := basicKind(v1)
arg1 = indirectInterface(arg1)
k1, err := basicKind(arg1)
if err != nil {
return false, err
}
v2 := indirectInterface(arg2)
k2, err := basicKind(v2)
arg2 = indirectInterface(arg2)
k2, err := basicKind(arg2)
if err != nil {
return false, err
}
@@ -514,9 +533,9 @@ func lt(arg1, arg2 reflect.Value) (bool, error) {
// Special case: Can compare integer values regardless of type's sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = v1.Int() < 0 || uint64(v1.Int()) < v2.Uint()
truth = arg1.Int() < 0 || uint64(arg1.Int()) < arg2.Uint()
case k1 == uintKind && k2 == intKind:
truth = v2.Int() >= 0 && v1.Uint() < uint64(v2.Int())
truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int())
default:
return false, errBadComparison
}
@@ -525,13 +544,13 @@ func lt(arg1, arg2 reflect.Value) (bool, error) {
case boolKind, complexKind:
return false, errBadComparisonType
case floatKind:
truth = v1.Float() < v2.Float()
truth = arg1.Float() < arg2.Float()
case intKind:
truth = v1.Int() < v2.Int()
truth = arg1.Int() < arg2.Int()
case stringKind:
truth = v1.String() < v2.String()
truth = arg1.String() < arg2.String()
case uintKind:
truth = v1.Uint() < v2.Uint()
truth = arg1.Uint() < arg2.Uint()
default:
panic("invalid kind")
}
@@ -634,8 +653,10 @@ var (
jsBackslash = []byte(`\\`)
jsApos = []byte(`\'`)
jsQuot = []byte(`\"`)
jsLt = []byte(`\x3C`)
jsGt = []byte(`\x3E`)
jsLt = []byte(`\u003C`)
jsGt = []byte(`\u003E`)
jsAmp = []byte(`\u0026`)
jsEq = []byte(`\u003D`)
)
// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
@@ -664,6 +685,10 @@ func JSEscape(w io.Writer, b []byte) {
w.Write(jsLt)
case '>':
w.Write(jsGt)
case '&':
w.Write(jsAmp)
case '=':
w.Write(jsEq)
default:
w.Write(jsLowUni)
t, b := c>>4, c&0x0f
@@ -698,7 +723,7 @@ func JSEscapeString(s string) string {
func jsIsSpecial(r rune) bool {
switch r {
case '\\', '\'', '"', '<', '>':
case '\\', '\'', '"', '<', '>', '&', '=':
return true
}
return r < ' ' || utf8.RuneSelf <= r

View File

@@ -30,7 +30,7 @@ package is auto generated.
*/
// Export it so we can populate Hugo's func map with it, which makes it faster.
var GoFuncs = builtinFuncs
var GoFuncs = builtinFuncs()
// Preparer prepares the template before execution.
type Preparer interface {

View File

@@ -244,7 +244,7 @@ func TestAddParseTree(t *testing.T) {
t.Fatal(err)
}
// Add a new parse tree.
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins())
if err != nil {
t.Fatal(err)
}

View File

@@ -411,7 +411,6 @@ func lexInsideAction(l *lexer) stateFn {
}
case r <= unicode.MaxASCII && unicode.IsPrint(r):
l.emit(itemChar)
return lexInsideAction
default:
return l.errorf("unrecognized character in action: %#U", r)
}

View File

@@ -7,7 +7,6 @@
package parse
import (
"bytes"
"fmt"
"strconv"
"strings"
@@ -29,6 +28,8 @@ type Node interface {
// tree returns the containing *Tree.
// It is unexported so all implementations of Node are in this package.
tree() *Tree
// writeTo writes the String output to the builder.
writeTo(*strings.Builder)
}
// NodeType identifies the type of a parse tree node.
@@ -94,11 +95,15 @@ func (l *ListNode) tree() *Tree {
}
func (l *ListNode) String() string {
b := new(bytes.Buffer)
var sb strings.Builder
l.writeTo(&sb)
return sb.String()
}
func (l *ListNode) writeTo(sb *strings.Builder) {
for _, n := range l.Nodes {
fmt.Fprint(b, n)
n.writeTo(sb)
}
return b.String()
}
func (l *ListNode) CopyList() *ListNode {
@@ -132,6 +137,10 @@ func (t *TextNode) String() string {
return fmt.Sprintf(textFormat, t.Text)
}
func (t *TextNode) writeTo(sb *strings.Builder) {
sb.WriteString(t.String())
}
func (t *TextNode) tree() *Tree {
return t.tr
}
@@ -160,23 +169,27 @@ func (p *PipeNode) append(command *CommandNode) {
}
func (p *PipeNode) String() string {
s := ""
var sb strings.Builder
p.writeTo(&sb)
return sb.String()
}
func (p *PipeNode) writeTo(sb *strings.Builder) {
if len(p.Decl) > 0 {
for i, v := range p.Decl {
if i > 0 {
s += ", "
sb.WriteString(", ")
}
s += v.String()
v.writeTo(sb)
}
s += " := "
sb.WriteString(" := ")
}
for i, c := range p.Cmds {
if i > 0 {
s += " | "
sb.WriteString(" | ")
}
s += c.String()
c.writeTo(sb)
}
return s
}
func (p *PipeNode) tree() *Tree {
@@ -187,9 +200,9 @@ func (p *PipeNode) CopyPipe() *PipeNode {
if p == nil {
return p
}
var vars []*VariableNode
for _, d := range p.Decl {
vars = append(vars, d.Copy().(*VariableNode))
vars := make([]*VariableNode, len(p.Decl))
for i, d := range p.Decl {
vars[i] = d.Copy().(*VariableNode)
}
n := p.tr.newPipeline(p.Pos, p.Line, vars)
n.IsAssign = p.IsAssign
@@ -219,8 +232,15 @@ func (t *Tree) newAction(pos Pos, line int, pipe *PipeNode) *ActionNode {
}
func (a *ActionNode) String() string {
return fmt.Sprintf("{{%s}}", a.Pipe)
var sb strings.Builder
a.writeTo(&sb)
return sb.String()
}
func (a *ActionNode) writeTo(sb *strings.Builder) {
sb.WriteString("{{")
a.Pipe.writeTo(sb)
sb.WriteString("}}")
}
func (a *ActionNode) tree() *Tree {
@@ -249,18 +269,24 @@ func (c *CommandNode) append(arg Node) {
}
func (c *CommandNode) String() string {
s := ""
var sb strings.Builder
c.writeTo(&sb)
return sb.String()
}
func (c *CommandNode) writeTo(sb *strings.Builder) {
for i, arg := range c.Args {
if i > 0 {
s += " "
sb.WriteByte(' ')
}
if arg, ok := arg.(*PipeNode); ok {
s += "(" + arg.String() + ")"
sb.WriteByte('(')
arg.writeTo(sb)
sb.WriteByte(')')
continue
}
s += arg.String()
arg.writeTo(sb)
}
return s
}
func (c *CommandNode) tree() *Tree {
@@ -311,6 +337,10 @@ func (i *IdentifierNode) String() string {
return i.Ident
}
func (i *IdentifierNode) writeTo(sb *strings.Builder) {
sb.WriteString(i.String())
}
func (i *IdentifierNode) tree() *Tree {
return i.tr
}
@@ -333,14 +363,18 @@ func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
}
func (v *VariableNode) String() string {
s := ""
var sb strings.Builder
v.writeTo(&sb)
return sb.String()
}
func (v *VariableNode) writeTo(sb *strings.Builder) {
for i, id := range v.Ident {
if i > 0 {
s += "."
sb.WriteByte('.')
}
s += id
sb.WriteString(id)
}
return s
}
func (v *VariableNode) tree() *Tree {
@@ -373,6 +407,10 @@ func (d *DotNode) String() string {
return "."
}
func (d *DotNode) writeTo(sb *strings.Builder) {
sb.WriteString(d.String())
}
func (d *DotNode) tree() *Tree {
return d.tr
}
@@ -403,6 +441,10 @@ func (n *NilNode) String() string {
return "nil"
}
func (n *NilNode) writeTo(sb *strings.Builder) {
sb.WriteString(n.String())
}
func (n *NilNode) tree() *Tree {
return n.tr
}
@@ -426,11 +468,16 @@ func (t *Tree) newField(pos Pos, ident string) *FieldNode {
}
func (f *FieldNode) String() string {
s := ""
var sb strings.Builder
f.writeTo(&sb)
return sb.String()
}
func (f *FieldNode) writeTo(sb *strings.Builder) {
for _, id := range f.Ident {
s += "." + id
sb.WriteByte('.')
sb.WriteString(id)
}
return s
}
func (f *FieldNode) tree() *Tree {
@@ -469,14 +516,23 @@ func (c *ChainNode) Add(field string) {
}
func (c *ChainNode) String() string {
s := c.Node.String()
var sb strings.Builder
c.writeTo(&sb)
return sb.String()
}
func (c *ChainNode) writeTo(sb *strings.Builder) {
if _, ok := c.Node.(*PipeNode); ok {
s = "(" + s + ")"
sb.WriteByte('(')
c.Node.writeTo(sb)
sb.WriteByte(')')
} else {
c.Node.writeTo(sb)
}
for _, field := range c.Field {
s += "." + field
sb.WriteByte('.')
sb.WriteString(field)
}
return s
}
func (c *ChainNode) tree() *Tree {
@@ -506,6 +562,10 @@ func (b *BoolNode) String() string {
return "false"
}
func (b *BoolNode) writeTo(sb *strings.Builder) {
sb.WriteString(b.String())
}
func (b *BoolNode) tree() *Tree {
return b.tr
}
@@ -639,6 +699,10 @@ func (n *NumberNode) String() string {
return n.Text
}
func (n *NumberNode) writeTo(sb *strings.Builder) {
sb.WriteString(n.String())
}
func (n *NumberNode) tree() *Tree {
return n.tr
}
@@ -666,6 +730,10 @@ func (s *StringNode) String() string {
return s.Quoted
}
func (s *StringNode) writeTo(sb *strings.Builder) {
sb.WriteString(s.String())
}
func (s *StringNode) tree() *Tree {
return s.tr
}
@@ -690,6 +758,10 @@ func (e *endNode) String() string {
return "{{end}}"
}
func (e *endNode) writeTo(sb *strings.Builder) {
sb.WriteString(e.String())
}
func (e *endNode) tree() *Tree {
return e.tr
}
@@ -718,6 +790,10 @@ func (e *elseNode) String() string {
return "{{else}}"
}
func (e *elseNode) writeTo(sb *strings.Builder) {
sb.WriteString(e.String())
}
func (e *elseNode) tree() *Tree {
return e.tr
}
@@ -738,6 +814,12 @@ type BranchNode struct {
}
func (b *BranchNode) String() string {
var sb strings.Builder
b.writeTo(&sb)
return sb.String()
}
func (b *BranchNode) writeTo(sb *strings.Builder) {
name := ""
switch b.NodeType {
case NodeIf:
@@ -749,10 +831,17 @@ func (b *BranchNode) String() string {
default:
panic("unknown branch type")
}
sb.WriteString("{{")
sb.WriteString(name)
sb.WriteByte(' ')
b.Pipe.writeTo(sb)
sb.WriteString("}}")
b.List.writeTo(sb)
if b.ElseList != nil {
return fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}", name, b.Pipe, b.List, b.ElseList)
sb.WriteString("{{else}}")
b.ElseList.writeTo(sb)
}
return fmt.Sprintf("{{%s %s}}%s{{end}}", name, b.Pipe, b.List)
sb.WriteString("{{end}}")
}
func (b *BranchNode) tree() *Tree {
@@ -826,10 +915,19 @@ func (t *Tree) newTemplate(pos Pos, line int, name string, pipe *PipeNode) *Temp
}
func (t *TemplateNode) String() string {
if t.Pipe == nil {
return fmt.Sprintf("{{template %q}}", t.Name)
var sb strings.Builder
t.writeTo(&sb)
return sb.String()
}
func (t *TemplateNode) writeTo(sb *strings.Builder) {
sb.WriteString("{{template ")
sb.WriteString(strconv.Quote(t.Name))
if t.Pipe != nil {
sb.WriteByte(' ')
t.Pipe.writeTo(sb)
}
return fmt.Sprintf("{{template %q %s}}", t.Name, t.Pipe)
sb.WriteString("}}")
}
func (t *TemplateNode) tree() *Tree {

View File

@@ -108,13 +108,8 @@ func (t *Tree) nextNonSpace() (token item) {
}
// peekNonSpace returns but does not consume the next non-space token.
func (t *Tree) peekNonSpace() (token item) {
for {
token = t.next()
if token.typ != itemSpace {
break
}
}
func (t *Tree) peekNonSpace() item {
token := t.nextNonSpace()
t.backup()
return token
}

View File

@@ -306,7 +306,8 @@ var parseTests = []parseTest{
}
var builtins = map[string]interface{}{
"printf": fmt.Sprintf,
"printf": fmt.Sprintf,
"contains": strings.Contains,
}
func testParse(doCopy bool, t *testing.T) {
@@ -555,3 +556,52 @@ func BenchmarkParseLarge(b *testing.B) {
}
}
}
var sinkv, sinkl string
func BenchmarkVariableString(b *testing.B) {
v := &VariableNode{
Ident: []string{"$", "A", "BB", "CCC", "THIS_IS_THE_VARIABLE_BEING_PROCESSED"},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sinkv = v.String()
}
if sinkv == "" {
b.Fatal("Benchmark was not run")
}
}
func BenchmarkListString(b *testing.B) {
text := `
{{(printf .Field1.Field2.Field3).Value}}
{{$x := (printf .Field1.Field2.Field3).Value}}
{{$y := (printf $x.Field1.Field2.Field3).Value}}
{{$z := $y.Field1.Field2.Field3}}
{{if contains $y $z}}
{{printf "%q" $y}}
{{else}}
{{printf "%q" $x}}
{{end}}
{{with $z.Field1 | contains "boring"}}
{{printf "%q" . | printf "%s"}}
{{else}}
{{printf "%d %d %d" 11 11 11}}
{{printf "%d %d %s" 22 22 $x.Field1.Field2.Field3 | printf "%s"}}
{{printf "%v" (contains $z.Field1.Field2 $y)}}
{{end}}
`
tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sinkl = tree.Root.String()
}
if sinkl == "" {
b.Fatal("Benchmark was not run")
}
}

View File

@@ -110,20 +110,21 @@ func (t *Template) Clone() (*Template, error) {
// copy returns a shallow copy of t, with common set to the argument.
func (t *Template) copy(c *common) *Template {
nt := New(t.name)
nt.Tree = t.Tree
nt.common = c
nt.leftDelim = t.leftDelim
nt.rightDelim = t.rightDelim
return nt
return &Template{
name: t.name,
Tree: t.Tree,
common: c,
leftDelim: t.leftDelim,
rightDelim: t.rightDelim,
}
}
// AddParseTree adds parse tree for template with given name and associates it with t.
// If the template does not already exist, it will create a new one.
// If the template does exist, it will be replaced.
// AddParseTree associates the argument parse tree with the template t, giving
// it the specified name. If the template has not been defined, this tree becomes
// its definition. If it has been defined and already has that name, the existing
// definition is replaced; otherwise a new template is created, defined, and returned.
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
t.init()
// If the name is the name of this template, overwrite this template.
nt := t
if name != t.name {
nt = t.New(name)
@@ -197,7 +198,7 @@ func (t *Template) Lookup(name string) *Template {
func (t *Template) Parse(text string) (*Template, error) {
t.init()
t.muFuncs.RLock()
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins)
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins())
t.muFuncs.RUnlock()
if err != nil {
return nil, err