shithub: hugo

Download patch

ref: 717f702e2fa32daf50170cdfa2d45b5d2304358b
parent: 8f6f871f539a24347388dec1927adf955248cb53
author: Derek Perkins <[email protected]>
date: Thu Dec 11 08:29:22 EST 2014

Added delimit & sort template functions, tests and docs

--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -74,6 +74,73 @@
        {{ .Content}}
     {{ end }}
 
+### delimit
+Loops through any array, slice or map and returns a string of all the values separated by the delimiter. There is an optional third parameter that lets you choose a different delimiter to go between the last two values.
+Maps will be sorted by the keys, and only a slice of the values will be returned, keeping a consistent output order.
+
+Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/)
+
+e.g.
+    // Front matter
+    +++
+    tags: [ "tag1", "tag2", "tag3" ]
+    +++
+
+    // Used anywhere in a template
+    Tags: {{ delimit .Params.tags ", " }}
+
+    // Outputs Tags: tag1, tag2, tag3
+
+    // Example with the optional "last" parameter
+    Tags: {{ delimit .Params.tags ", " " and " }}
+
+    // Outputs Tags: tag1, tag2 and tag3
+
+### sort
+Sorts maps, arrays and slices, returning a sorted slice. A sorted array of map values will be returned, with the keys eliminated. There are two optional arguments, which are `sortByField` and `sortAsc`. If left blank, sort will sort by keys (for maps) in ascending order.
+
+Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/)
+
+e.g.
+    // Front matter
+    +++
+    tags: [ "tag3", "tag1", "tag2" ]
+    +++
+
+    // Site config
+    +++
+    [params.authors]
+      [params.authors.Derek]
+        "firstName"  = "Derek"
+        "lastName"   = "Perkins"
+      [params.authors.Joe]
+        "firstName"  = "Joe"
+        "lastName"   = "Bergevin"
+      [params.authors.Tanner]
+        "firstName"  = "Tanner"
+        "lastName"   = "Linsley"
+    +++
+
+    // Use default sort options - sort by key / ascending
+    Tags: {{ range sort .Params.tags }}{{ . }} {{ end }}
+
+    // Outputs Tags: tag1 tag2 tag3
+
+    // Sort by value / descending
+    Tags: {{ range sort .Params.tags "value" "desc" }}{{ . }} {{ end }}
+
+    // Outputs Tags: tag3 tag2 tag1
+
+    // Use default sort options - sort by value / descending
+    Authors: {{ range sort .Site.Params.authors }}{{ .firstName }} {{ end }}
+
+    // Outputs Authors: Derek Joe Tanner
+
+    // Use default sort options - sort by value / descending
+    Authors: {{ range sort .Site.Params.authors "lastName" "desc" }}{{ .lastName }} {{ end }}
+
+    // Outputs Authors: Perkins Linsley Bergevin
+
 ### in
 Checks if an element is in an array (or slice) and returns a boolean.  The elements supported are strings, integers and floats (only float64 will match as expected).  In addition, it can also check if a substring exists in a string.
 
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -29,6 +29,7 @@
 	"os"
 	"path/filepath"
 	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 )
@@ -100,6 +101,8 @@
 		"markdownify": Markdownify,
 		"first":       First,
 		"where":       Where,
+		"delimit":     Delimit,
+		"sort":        Sort,
 		"highlight":   Highlight,
 		"add":         func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '+') },
 		"sub":         func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '-') },
@@ -150,6 +153,8 @@
 
 func compareGetFloat(a interface{}, b interface{}) (float64, float64) {
 	var left, right float64
+	var leftStr, rightStr *string
+	var err error
 	av := reflect.ValueOf(a)
 
 	switch av.Kind() {
@@ -160,7 +165,11 @@
 	case reflect.Float32, reflect.Float64:
 		left = av.Float()
 	case reflect.String:
-		left, _ = strconv.ParseFloat(av.String(), 64)
+		left, err = strconv.ParseFloat(av.String(), 64)
+		if err != nil {
+			str := av.String()
+			leftStr = &str
+		}
 	}
 
 	bv := reflect.ValueOf(b)
@@ -173,9 +182,24 @@
 	case reflect.Float32, reflect.Float64:
 		right = bv.Float()
 	case reflect.String:
-		right, _ = strconv.ParseFloat(bv.String(), 64)
+		right, err = strconv.ParseFloat(bv.String(), 64)
+		if err != nil {
+			str := bv.String()
+			rightStr = &str
+		}
+
 	}
 
+	switch {
+	case leftStr == nil || rightStr == nil:
+	case *leftStr < *rightStr:
+		return 0, 1
+	case *leftStr > *rightStr:
+		return 1, 0
+	default:
+		return 0, 0
+	}
+
 	return left, right
 }
 
@@ -375,6 +399,173 @@
 	default:
 		return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String())
 	}
+}
+
+func Delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) {
+	d, err := cast.ToStringE(delimiter)
+	if err != nil {
+		return "", err
+	}
+
+	var dLast *string
+	for _, l := range last {
+		dStr, err := cast.ToStringE(l)
+		if err != nil {
+			dLast = nil
+		}
+		dLast = &dStr
+		break
+	}
+
+	seqv := reflect.ValueOf(seq)
+	for ; seqv.Kind() == reflect.Ptr || seqv.Kind() == reflect.Interface; seqv = seqv.Elem() {
+		if seqv.IsNil() {
+			return "", errors.New("can't iterate over a nil value")
+		}
+		if seqv.Kind() == reflect.Interface && seqv.NumMethod() > 0 {
+			break
+		}
+	}
+
+	var str string
+	switch seqv.Kind() {
+	case reflect.Map:
+		sortSeq, err := Sort(seq)
+		if err != nil {
+			return "", err
+		}
+		seqv = reflect.ValueOf(sortSeq)
+		fallthrough
+	case reflect.Array, reflect.Slice, reflect.String:
+		for i := 0; i < seqv.Len(); i++ {
+			val := seqv.Index(i).Interface()
+			valStr, err := cast.ToStringE(val)
+			if err != nil {
+				continue
+			}
+			switch {
+			case i == seqv.Len()-2 && dLast != nil:
+				str += valStr + *dLast
+			case i == seqv.Len()-1:
+				str += valStr
+			default:
+				str += valStr + d
+			}
+		}
+
+	default:
+		return "", errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String())
+	}
+
+	return template.HTML(str), nil
+}
+
+func Sort(seq interface{}, args ...interface{}) ([]interface{}, error) {
+	seqv := reflect.ValueOf(seq)
+	for ; seqv.Kind() == reflect.Ptr || seqv.Kind() == reflect.Interface; seqv = seqv.Elem() {
+		if seqv.IsNil() {
+			return nil, errors.New("can't iterate over a nil value")
+		}
+		if seqv.Kind() == reflect.Interface && seqv.NumMethod() > 0 {
+			break
+		}
+	}
+
+	// Create a list of pairs that will be used to do the sort
+	p := pairList{SortAsc: true}
+	p.Pairs = make([]pair, seqv.Len())
+
+	for i, l := range args {
+		dStr, err := cast.ToStringE(l)
+		switch {
+		case i == 0 && err != nil:
+			p.SortByField = ""
+		case i == 0 && err == nil:
+			p.SortByField = dStr
+		case i == 1 && err == nil && dStr == "desc":
+			p.SortAsc = false
+		case i == 1:
+			p.SortAsc = true
+		}
+	}
+
+	var sorted []interface{}
+	switch seqv.Kind() {
+	case reflect.Array, reflect.Slice:
+		for i := 0; i < seqv.Len(); i++ {
+			p.Pairs[i].Key = reflect.ValueOf(i)
+			p.Pairs[i].Value = seqv.Index(i)
+		}
+		if p.SortByField == "" {
+			p.SortByField = "value"
+		}
+
+	case reflect.Map:
+		keys := seqv.MapKeys()
+		for i := 0; i < seqv.Len(); i++ {
+			p.Pairs[i].Key = keys[i]
+			p.Pairs[i].Value = seqv.MapIndex(keys[i])
+		}
+
+	default:
+		return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String())
+	}
+	sorted = p.sort()
+	return sorted, nil
+}
+
+// Credit for pair sorting method goes to Andrew Gerrand
+// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw
+// A data structure to hold a key/value pair.
+type pair struct {
+	Key   reflect.Value
+	Value reflect.Value
+}
+
+// A slice of pairs that implements sort.Interface to sort by Value.
+type pairList struct {
+	Pairs       []pair
+	SortByField string
+	SortAsc     bool
+}
+
+func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] }
+func (p pairList) Len() int      { return len(p.Pairs) }
+func (p pairList) Less(i, j int) bool {
+	var truth bool
+	switch {
+	case p.SortByField == "value":
+		iVal := p.Pairs[i].Value
+		jVal := p.Pairs[j].Value
+		truth = Lt(iVal.Interface(), jVal.Interface())
+
+	case p.SortByField != "":
+		if p.Pairs[i].Value.FieldByName(p.SortByField).IsValid() {
+			iVal := p.Pairs[i].Value.FieldByName(p.SortByField)
+			jVal := p.Pairs[j].Value.FieldByName(p.SortByField)
+			truth = Lt(iVal.Interface(), jVal.Interface())
+		}
+	default:
+		iVal := p.Pairs[i].Key
+		jVal := p.Pairs[j].Key
+		truth = Lt(iVal.Interface(), jVal.Interface())
+	}
+	return truth
+}
+
+// sorts a pairList and returns a slice of sorted values
+func (p pairList) sort() []interface{} {
+	if p.SortAsc {
+		sort.Sort(p)
+	} else {
+		sort.Sort(sort.Reverse(p))
+	}
+	sorted := make([]interface{}, len(p.Pairs))
+	for i, v := range p.Pairs {
+		sorted[i] = v.Value.Interface()
+	}
+
+	return sorted
 }
 
 func IsSet(a interface{}, key interface{}) bool {
--- a/tpl/template_test.go
+++ b/tpl/template_test.go
@@ -341,6 +341,123 @@
 	}
 }
 
+func TestDelimit(t *testing.T) {
+	for i, this := range []struct {
+		sequence  interface{}
+		delimiter interface{}
+		last      interface{}
+		expect    template.HTML
+	}{
+		{[]string{"class1", "class2", "class3"}, " ", nil, "class1 class2 class3"},
+		{[]int{1, 2, 3, 4, 5}, ",", nil, "1,2,3,4,5"},
+		{[]int{1, 2, 3, 4, 5}, ", ", nil, "1, 2, 3, 4, 5"},
+		{[]string{"class1", "class2", "class3"}, " ", " and ", "class1 class2 and class3"},
+		{[]int{1, 2, 3, 4, 5}, ",", ",", "1,2,3,4,5"},
+		{[]int{1, 2, 3, 4, 5}, ", ", ", and ", "1, 2, 3, 4, and 5"},
+		// test maps with and without sorting required
+		{map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", nil, "10--20--30--40--50"},
+		{map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", nil, "30--20--10--40--50"},
+		{map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", nil, "10--20--30--40--50"},
+		{map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", nil, "30--20--10--40--50"},
+		{map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", nil, "50--40--10--30--20"},
+		{map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", nil, "10--20--30--40--50"},
+		{map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", nil, "30--20--10--40--50"},
+		{map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, "--", nil, "30--20--10--40--50"},
+		// test maps with a last delimiter
+		{map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", "--and--", "10--20--30--40--and--50"},
+		{map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", "--and--", "30--20--10--40--and--50"},
+		{map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", "--and--", "10--20--30--40--and--50"},
+		{map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", "--and--", "30--20--10--40--and--50"},
+		{map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", "--and--", "50--40--10--30--and--20"},
+		{map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", "--and--", "10--20--30--40--and--50"},
+		{map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", "--and--", "30--20--10--40--and--50"},
+		{map[float64]string{3.5: "10", 2.5: "20", 1.5: "30", 4.5: "40", 5.5: "50"}, "--", "--and--", "30--20--10--40--and--50"},
+	} {
+		var result template.HTML
+		var err error
+		if this.last == nil {
+			result, err = Delimit(this.sequence, this.delimiter)
+		} else {
+			result, err = Delimit(this.sequence, this.delimiter, this.last)
+		}
+		if err != nil {
+			t.Errorf("[%d] failed: %s", i, err)
+			continue
+		}
+		if !reflect.DeepEqual(result, this.expect) {
+			t.Errorf("[%d] Delimit called on sequence: %v | delimiter: `%v` | last: `%v`, got %v but expected %v", i, this.sequence, this.delimiter, this.last, result, this.expect)
+		}
+	}
+}
+
+func TestSort(t *testing.T) {
+	type ts struct {
+		MyInt    int
+		MyFloat  float64
+		MyString string
+	}
+	for i, this := range []struct {
+		sequence    interface{}
+		sortByField interface{}
+		sortAsc     string
+		expect      []interface{}
+	}{
+		{[]string{"class1", "class2", "class3"}, nil, "asc", []interface{}{"class1", "class2", "class3"}},
+		{[]string{"class3", "class1", "class2"}, nil, "asc", []interface{}{"class1", "class2", "class3"}},
+		{[]int{1, 2, 3, 4, 5}, nil, "asc", []interface{}{1, 2, 3, 4, 5}},
+		{[]int{5, 4, 3, 1, 2}, nil, "asc", []interface{}{1, 2, 3, 4, 5}},
+		// test map sorting by keys
+		{map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []interface{}{10, 20, 30, 40, 50}},
+		{map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []interface{}{30, 20, 10, 40, 50}},
+		{map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, nil, "asc", []interface{}{"10", "20", "30", "40", "50"}},
+		{map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, nil, "asc", []interface{}{"30", "20", "10", "40", "50"}},
+		{map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, nil, "asc", []interface{}{"50", "40", "10", "30", "20"}},
+		{map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, nil, "asc", []interface{}{"10", "20", "30", "40", "50"}},
+		{map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, nil, "asc", []interface{}{"30", "20", "10", "40", "50"}},
+		{map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, nil, "asc", []interface{}{"30", "20", "10", "40", "50"}},
+		// test map sorting by value
+		{map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "value", "asc", []interface{}{10, 20, 30, 40, 50}},
+		{map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "value", "asc", []interface{}{10, 20, 30, 40, 50}},
+		// test map sorting by field value
+		{
+			map[string]ts{"1": ts{10, 10.5, "ten"}, "2": ts{20, 20.5, "twenty"}, "3": ts{30, 30.5, "thirty"}, "4": ts{40, 40.5, "forty"}, "5": ts{50, 50.5, "fifty"}},
+			"MyInt",
+			"asc",
+			[]interface{}{ts{10, 10.5, "ten"}, ts{20, 20.5, "twenty"}, ts{30, 30.5, "thirty"}, ts{40, 40.5, "forty"}, ts{50, 50.5, "fifty"}},
+		},
+		{
+			map[string]ts{"1": ts{10, 10.5, "ten"}, "2": ts{20, 20.5, "twenty"}, "3": ts{30, 30.5, "thirty"}, "4": ts{40, 40.5, "forty"}, "5": ts{50, 50.5, "fifty"}},
+			"MyFloat",
+			"asc",
+			[]interface{}{ts{10, 10.5, "ten"}, ts{20, 20.5, "twenty"}, ts{30, 30.5, "thirty"}, ts{40, 40.5, "forty"}, ts{50, 50.5, "fifty"}},
+		},
+		{
+			map[string]ts{"1": ts{10, 10.5, "ten"}, "2": ts{20, 20.5, "twenty"}, "3": ts{30, 30.5, "thirty"}, "4": ts{40, 40.5, "forty"}, "5": ts{50, 50.5, "fifty"}},
+			"MyString",
+			"asc",
+			[]interface{}{ts{50, 50.5, "fifty"}, ts{40, 40.5, "forty"}, ts{10, 10.5, "ten"}, ts{30, 30.5, "thirty"}, ts{20, 20.5, "twenty"}},
+		},
+		// Test sort desc
+		{[]string{"class1", "class2", "class3"}, "value", "desc", []interface{}{"class3", "class2", "class1"}},
+		{[]string{"class3", "class1", "class2"}, "value", "desc", []interface{}{"class3", "class2", "class1"}},
+	} {
+		var result []interface{}
+		var err error
+		if this.sortByField == nil {
+			result, err = Sort(this.sequence)
+		} else {
+			result, err = Sort(this.sequence, this.sortByField, this.sortAsc)
+		}
+		if err != nil {
+			t.Errorf("[%d] failed: %s", i, err)
+			continue
+		}
+		if !reflect.DeepEqual(result, this.expect) {
+			t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, this.sequence, this.sortByField, result, this.expect)
+		}
+	}
+}
+
 func TestMarkdownify(t *testing.T) {
 
 	result := Markdownify("Hello **World!**")