github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/tpl/collections/merge.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package collections
    15  
    16  import (
    17  	"fmt"
    18  	"reflect"
    19  	"strings"
    20  
    21  	"github.com/gohugoio/hugo/common/hreflect"
    22  	"github.com/gohugoio/hugo/common/maps"
    23  
    24  	"errors"
    25  )
    26  
    27  // Merge creates a copy of the final parameter in params and merges the preceding
    28  // parameters into it in reverse order.
    29  //
    30  // Currently only maps are supported. Key handling is case insensitive.
    31  func (ns *Namespace) Merge(params ...any) (any, error) {
    32  	if len(params) < 2 {
    33  		return nil, errors.New("merge requires at least two parameters")
    34  	}
    35  
    36  	var err error
    37  	result := params[len(params)-1]
    38  
    39  	for i := len(params) - 2; i >= 0; i-- {
    40  		result, err = ns.merge(params[i], result)
    41  		if err != nil {
    42  			return nil, err
    43  		}
    44  	}
    45  
    46  	return result, nil
    47  }
    48  
    49  // merge creates a copy of dst and merges src into it.
    50  func (ns *Namespace) merge(src, dst any) (any, error) {
    51  	vdst, vsrc := reflect.ValueOf(dst), reflect.ValueOf(src)
    52  
    53  	if vdst.Kind() != reflect.Map {
    54  		return nil, fmt.Errorf("destination must be a map, got %T", dst)
    55  	}
    56  
    57  	if !hreflect.IsTruthfulValue(vsrc) {
    58  		return dst, nil
    59  	}
    60  
    61  	if vsrc.Kind() != reflect.Map {
    62  		return nil, fmt.Errorf("source must be a map, got %T", src)
    63  	}
    64  
    65  	if vsrc.Type().Key() != vdst.Type().Key() {
    66  		return nil, fmt.Errorf("incompatible map types, got %T to %T", src, dst)
    67  	}
    68  
    69  	return mergeMap(vdst, vsrc).Interface(), nil
    70  }
    71  
    72  func caseInsensitiveLookup(m, k reflect.Value) (reflect.Value, bool) {
    73  	if m.Type().Key().Kind() != reflect.String || k.Kind() != reflect.String {
    74  		// Fall back to direct lookup.
    75  		v := m.MapIndex(k)
    76  		return v, hreflect.IsTruthfulValue(v)
    77  	}
    78  
    79  	for _, key := range m.MapKeys() {
    80  		if strings.EqualFold(k.String(), key.String()) {
    81  			return m.MapIndex(key), true
    82  		}
    83  	}
    84  
    85  	return reflect.Value{}, false
    86  }
    87  
    88  func mergeMap(dst, src reflect.Value) reflect.Value {
    89  	out := reflect.MakeMap(dst.Type())
    90  
    91  	// If the destination is Params, we must lower case all keys.
    92  	_, lowerCase := dst.Interface().(maps.Params)
    93  
    94  	// Copy the destination map.
    95  	for _, key := range dst.MapKeys() {
    96  		v := dst.MapIndex(key)
    97  		out.SetMapIndex(key, v)
    98  	}
    99  
   100  	// Add all keys in src not already in destination.
   101  	// Maps of the same type will be merged.
   102  	for _, key := range src.MapKeys() {
   103  		sv := src.MapIndex(key)
   104  		dv, found := caseInsensitiveLookup(dst, key)
   105  
   106  		if found {
   107  			// If both are the same map key type, merge.
   108  			dve := dv.Elem()
   109  			if dve.Kind() == reflect.Map {
   110  				sve := sv.Elem()
   111  				if sve.Kind() != reflect.Map {
   112  					continue
   113  				}
   114  
   115  				if dve.Type().Key() == sve.Type().Key() {
   116  					out.SetMapIndex(key, mergeMap(dve, sve))
   117  				}
   118  			}
   119  		} else {
   120  			if lowerCase && key.Kind() == reflect.String {
   121  				key = reflect.ValueOf(strings.ToLower(key.String()))
   122  			}
   123  			out.SetMapIndex(key, sv)
   124  		}
   125  	}
   126  
   127  	return out
   128  }