diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md index c1a0eb64b..819a5c7a9 100644 --- a/docs/content/templates/functions.md +++ b/docs/content/templates/functions.md @@ -74,6 +74,73 @@ e.g. {{ .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. diff --git a/tpl/template.go b/tpl/template.go index bd700f6ab..daa8d7dd8 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -29,6 +29,7 @@ import ( "os" "path/filepath" "reflect" + "sort" "strconv" "strings" ) @@ -100,6 +101,8 @@ func New() Template { "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 Lt(a, b interface{}) bool { 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 @@ func compareGetFloat(a interface{}, b interface{}) (float64, float64) { 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,7 +182,22 @@ func compareGetFloat(a interface{}, b interface{}) (float64, float64) { 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 @@ -377,6 +401,173 @@ func Where(seq, key, match interface{}) (interface{}, error) { } } +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 { av := reflect.ValueOf(a) kv := reflect.ValueOf(key) diff --git a/tpl/template_test.go b/tpl/template_test.go index 066c75dde..30d721b6c 100644 --- a/tpl/template_test.go +++ b/tpl/template_test.go @@ -341,6 +341,123 @@ func TestWhere(t *testing.T) { } } +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!**")