// Copyright 2017 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package collections import ( "errors" "fmt" "reflect" "github.com/spf13/cast" "github.com/gohugoio/hugo/common/maps" ) // 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. // // Adapted from Go stdlib src/text/template/funcs.go. // // We deviate from the stdlib mostly because of https://github.com/golang/go/issues/14751. func (ns *Namespace) Index(item any, args ...any) (any, error) { v, err := ns.doIndex(item, args...) if err != nil { return nil, fmt.Errorf("index of type %T with args %v failed: %s", item, args, err) } return v, nil } func (ns *Namespace) doIndex(item any, args ...any) (any, error) { // TODO(moorereason): merge upstream changes. v := reflect.ValueOf(item) if !v.IsValid() { // See issue 10489 // This used to be an error. return nil, nil } var indices []any if len(args) == 1 { v := reflect.ValueOf(args[0]) if v.Kind() == reflect.Slice { for i := 0; i < v.Len(); i++ { indices = append(indices, v.Index(i).Interface()) } } else { indices = append(indices, args[0]) } } else { indices = args } lowerm, ok := item.(maps.Params) if ok { return lowerm.Get(cast.ToStringSlice(indices)...), nil } for _, i := range indices { index := reflect.ValueOf(i) var isNil bool if v, isNil = indirect(v); isNil { // See issue 10489 // This used to be an error. return nil, nil } 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, errors.New("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 } // 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 } // 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 }