diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index 3e87ab058..c0ed9bb31 100644 --- a/tpl/template_funcs.go +++ b/tpl/template_funcs.go @@ -1293,6 +1293,89 @@ func dfault(dflt interface{}, given ...interface{}) (interface{}, error) { return dflt, nil } +// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. +// +// Copied from Go stdlib src/text/template/exec.go. +func canBeNil(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return true + } + return false +} + +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +// +// Copied from Go stdlib src/text/template/funcs.go. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !canBeNil(argType) { + return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) + } + value = reflect.Zero(argType) + } + if !value.Type().AssignableTo(argType) { + return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) + } + return value, nil +} + +// index returns the result of indexing its first argument by the following +// 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. +// +// Copied from Go stdlib src/text/template/funcs.go. +func index(item interface{}, indices ...interface{}) (interface{}, error) { + v := reflect.ValueOf(item) + if !v.IsValid() { + return nil, fmt.Errorf("index of untyped nil") + } + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return nil, fmt.Errorf("index of nil pointer") + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + case reflect.Invalid: + return nil, fmt.Errorf("cannot index slice/array with nil") + default: + return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) + } + if x < 0 || x >= int64(v.Len()) { + // We deviate from stdlib here. Don't return an error if the + // index is out of range. + return nil, nil + } + v = v.Index(int(x)) + case reflect.Map: + index, err := prepareArg(index, v.Type().Key()) + if err != nil { + return nil, err + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + case reflect.Invalid: + // the loop holds invariant: v.IsValid() + panic("unreachable") + default: + return nil, fmt.Errorf("can't index item of type %s", v.Type()) + } + } + return v.Interface(), nil +} + // safeHTMLAttr returns a given string as html/template HTMLAttr content. // // safeHTMLAttr is currently disabled, pending further discussion @@ -1608,6 +1691,7 @@ func init() { "highlight": highlight, "humanize": humanize, "in": in, + "index": index, "int": func(v interface{}) int { return cast.ToInt(v) }, "intersect": intersect, "isSet": isSet, diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go index 764478e3b..96d0c013b 100644 --- a/tpl/template_funcs_test.go +++ b/tpl/template_funcs_test.go @@ -1911,6 +1911,7 @@ func TestDefault(t *testing.T) { {map[string]string{"foo": "pop"}, `{{ index . "bar" | default "nada" }}`, `nada`, true}, {map[string]string{"foo": "cat"}, `{{ default "nope" .foo }}`, `cat`, true}, {map[string]string{"foo": "dog"}, `{{ default "nope" .foo "extra" }}`, ``, false}, + {map[string]interface{}{"images": []string{}}, `{{ default "default.jpg" (index .images 0) }}`, `default.jpg`, true}, } { tmpl, err := New().New("test").Parse(this.tpl) if err != nil {