ref: 45df4596bb065c944c1c16d3c5be8042f64ebba2
parent: e95f3af933ea58df0389facca9bfde1119ef2bf4
author: Cameron Moore <moorereason@gmail.com>
date: Thu Feb 4 19:05:04 EST 2016
tpl: Add humanize func and cleanup lint Add humanize (inflect.Humanize) to the template funcMap. Documentation and tests are included. Various code cleanups of the template funcs: - Break pluralize and singularize out into stand-alone funcs. - Sort the list of funcMap entries. - Add some minimal godoc comments to all public funcs. - Fix some issues found by golint and grind.
--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -387,6 +387,16 @@
Used in the [highlight shortcode](/extras/highlighting/).
+### humanize
+Humanize returns the humanized version of a string with the first letter capitalized.
+
+e.g.
+```
+{{humanize "my-first-post"}} → "My first post"+{{humanize "myCamelPost"}} → "My camel post"+```
+
+
### lower
Converts all characters in string to lowercase.
--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -38,6 +38,7 @@
var funcMap template.FuncMap
+// Eq returns the boolean truth of arg1 == arg2.
func Eq(x, y interface{}) bool { normalize := func(v interface{}) interface{} {vv := reflect.ValueOf(v)
@@ -57,30 +58,38 @@
return reflect.DeepEqual(x, y)
}
+// Ne returns the boolean truth of arg1 != arg2.
func Ne(x, y interface{}) bool {return !Eq(x, y)
}
+// Ge returns the boolean truth of arg1 >= arg2.
func Ge(a, b interface{}) bool {left, right := compareGetFloat(a, b)
return left >= right
}
+// Gt returns the boolean truth of arg1 > arg2.
func Gt(a, b interface{}) bool {left, right := compareGetFloat(a, b)
return left > right
}
+// Le returns the boolean truth of arg1 <= arg2.
func Le(a, b interface{}) bool {left, right := compareGetFloat(a, b)
return left <= right
}
+// Lt returns the boolean truth of arg1 < arg2.
func Lt(a, b interface{}) bool {left, right := compareGetFloat(a, b)
return left < right
}
+// Dictionary creates a map[string]interface{} from the given parameters by+// walking the parameters and treating them as key-value pairs. The number
+// of parameters must be even.
func Dictionary(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, errors.New("invalid dict call")@@ -99,7 +108,6 @@
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() {@@ -110,6 +118,7 @@
case reflect.Float32, reflect.Float64:
left = av.Float()
case reflect.String:
+ var err error
left, err = strconv.ParseFloat(av.String(), 64)
if err != nil {str := av.String()
@@ -132,6 +141,7 @@
case reflect.Float32, reflect.Float64:
right = bv.Float()
case reflect.String:
+ var err error
right, err = strconv.ParseFloat(bv.String(), 64)
if err != nil {str := bv.String()
@@ -157,7 +167,7 @@
return left, right
}
-// Slicing in Slicestr is done by specifying a half-open range with
+// Slicestr slices a string by specifying a half-open range with
// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3.
// The end index can be omitted, it defaults to the string's length.
func Slicestr(a interface{}, startEnd ...interface{}) (string, error) {@@ -249,7 +259,7 @@
start = 0
}
if start > len(asRunes) {- return "", errors.New(fmt.Sprintf("start position out of bounds for %d-byte string", len(aStr)))+ return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr))}
var s, e int
@@ -268,7 +278,7 @@
}
if s > e {- return "", errors.New(fmt.Sprintf("calculated start position greater than end position: %d > %d", s, e))+ return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e)}
if e > len(asRunes) {e = len(asRunes)
@@ -275,9 +285,9 @@
}
return string(asRunes[s:e]), nil
-
}
+// Split slices an input string into all substrings separated by delimiter.
func Split(a interface{}, delimiter string) ([]string, error) {aStr, err := cast.ToStringE(a)
if err != nil {@@ -286,6 +296,8 @@
return strings.Split(aStr, delimiter), nil
}
+// Intersect returns the common elements in the given sets, l1 and l2. l1 and
+// l2 must be of the same type and may be either arrays or slices.
func Intersect(l1, l2 interface{}) (interface{}, error) { if l1 == nil || l2 == nil { return make([]interface{}, 0), nil@@ -334,6 +346,7 @@
}
}
+// In returns whether v is in the set l. l may be an array or slice.
func In(l interface{}, v interface{}) bool {lv := reflect.ValueOf(l)
vv := reflect.ValueOf(v)
@@ -388,10 +401,8 @@
return v, false
}
-// First is exposed to templates, to iterate over the first N items in a
-// rangeable list.
+// First returns the first N items in a rangeable list.
func First(limit interface{}, seq interface{}) (interface{}, error) {-
if limit == nil || seq == nil { return nil, errors.New("both limit and seq must be provided")}
@@ -424,10 +435,8 @@
return seqv.Slice(0, limitv).Interface(), nil
}
-// Last is exposed to templates, to iterate over the last N items in a
-// rangeable list.
+// Last returns the last N items in a rangeable list.
func Last(limit interface{}, seq interface{}) (interface{}, error) {-
if limit == nil || seq == nil { return nil, errors.New("both limit and seq must be provided")}
@@ -460,10 +469,8 @@
return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil
}
-// After is exposed to templates, to iterate over all the items after N in a
-// rangeable list. It's meant to accompany First
+// After returns all the items after the first N in a rangeable list.
func After(index interface{}, seq interface{}) (interface{}, error) {-
if index == nil || seq == nil { return nil, errors.New("both limit and seq must be provided")}
@@ -496,10 +503,8 @@
return seqv.Slice(indexv, seqv.Len()).Interface(), nil
}
-// Shuffle is exposed to templates, to iterate over items in rangeable list in
-// a randomised order.
+// Shuffle returns the given rangeable list in a randomised order.
func Shuffle(seq interface{}) (interface{}, error) {-
if seq == nil { return nil, errors.New("both count and seq must be provided")}
@@ -742,9 +747,8 @@
}
if op == "not in" {return !r, nil
- } else {- return r, nil
}
+ return r, nil
default:
return false, errors.New("no such an operator")}
@@ -751,6 +755,7 @@
return false, nil
}
+// Where returns a filtered subset of a given data type.
func Where(seq, key interface{}, args ...interface{}) (r interface{}, err error) {seqv := reflect.ValueOf(seq)
kv := reflect.ValueOf(key)
@@ -813,7 +818,7 @@
}
}
-// Apply, given a map, array, or slice, returns a new slice with the function fname applied over it.
+// Apply takes a map, array, or slice and returns a new slice with the function fname applied over it.
func Apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { if seq == nil { return make([]interface{}, 0), nil@@ -890,11 +895,12 @@
if len(res) == 1 || res[1].IsNil() {return res[0], nil
- } else {- return reflect.ValueOf(nil), res[1].Interface().(error)
}
+ return reflect.ValueOf(nil), res[1].Interface().(error)
}
+// Delimit takes a given sequence and returns a delimited HTML string.
+// If last is passed to the function, it will be used as the final delimiter.
func Delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) {d, err := cast.ToStringE(delimiter)
if err != nil {@@ -950,6 +956,7 @@
return template.HTML(str), nil
}
+// Sort returns a sorted sequence.
func Sort(seq interface{}, args ...interface{}) (interface{}, error) {seqv := reflect.ValueOf(seq)
seqv, isNil := indirect(seqv)
@@ -1066,6 +1073,8 @@
return sorted.Interface()
}
+// IsSet returns whether a given array, channel, slice, or map has a key
+// defined.
func IsSet(a interface{}, key interface{}) bool {av := reflect.ValueOf(a)
kv := reflect.ValueOf(key)
@@ -1084,6 +1093,8 @@
return false
}
+// ReturnWhenSet returns a given value if it set. Otherwise, it returns an
+// empty string.
func ReturnWhenSet(a, k interface{}) interface{} {av, isNil := indirect(reflect.ValueOf(a))
if isNil {@@ -1120,6 +1131,7 @@
return ""
}
+// Highlight returns an HTML string with syntax highlighting applied.
func Highlight(in interface{}, lang, opts string) template.HTML {var str string
av := reflect.ValueOf(in)
@@ -1134,6 +1146,7 @@
var markdownTrimPrefix = []byte("<p>") var markdownTrimSuffix = []byte("</p>\n")+// Markdownify renders a given string from Markdown to HTML.
func Markdownify(text string) template.HTML { m := helpers.RenderBytes(&helpers.RenderingContext{Content: []byte(text), PageFmt: "markdown"})m = bytes.TrimPrefix(m, markdownTrimPrefix)
@@ -1168,14 +1181,17 @@
return template.HTML(ref)
}
+// Ref returns the absolute URL path to a given content item.
func Ref(page interface{}, ref string) template.HTML {return refPage(page, ref, "Ref")
}
+// RelRef returns the relative URL path to a given content item.
func RelRef(page interface{}, ref string) template.HTML {return refPage(page, ref, "RelRef")
}
+// Chomp removes trailing newline characters from a string.
func Chomp(text interface{}) (string, error) {s, err := cast.ToStringE(text)
if err != nil {@@ -1222,23 +1238,28 @@
return t.Format(layout), nil
}
-// "safeHTMLAttr" is currently disabled, pending further discussion
+// SafeHTMLAttr returns a given string as html/template HTMLAttr content.
+//
+// SafeHTMLAttr is currently disabled, pending further discussion
// on its use case. 2015-01-19
func SafeHTMLAttr(text string) template.HTMLAttr {return template.HTMLAttr(text)
}
+// SafeCSS returns a given string as html/template CSS content.
func SafeCSS(text string) template.CSS {return template.CSS(text)
}
+// SafeURL returns a given string as html/template URL content.
func SafeURL(text string) template.URL {return template.URL(text)
}
+// SafeHTML returns a given string as html/template HTML content.
func SafeHTML(a string) template.HTML { return template.HTML(a) }-// SafeJS returns the given string as a template.JS type from html/template.
+// SafeJS returns the given string as a html/template JS content.
func SafeJS(a string) template.JS { return template.JS(a) } func doArithmetic(a, b interface{}, op rune) (interface{}, error) {@@ -1307,9 +1328,8 @@
if bv.Kind() == reflect.String && op == '+' {bs := bv.String()
return as + bs, nil
- } else {- return nil, errors.New("Can't apply the operator to the values")}
+ return nil, errors.New("Can't apply the operator to the values")default:
return nil, errors.New("Can't apply the operator to the values")}
@@ -1322,9 +1342,8 @@
return af + bf, nil
} else if au != 0 || bu != 0 {return au + bu, nil
- } else {- return 0, nil
}
+ return 0, nil
case '-':
if ai != 0 || bi != 0 {return ai - bi, nil
@@ -1332,9 +1351,8 @@
return af - bf, nil
} else if au != 0 || bu != 0 {return au - bu, nil
- } else {- return 0, nil
}
+ return 0, nil
case '*':
if ai != 0 || bi != 0 {return ai * bi, nil
@@ -1342,9 +1360,8 @@
return af * bf, nil
} else if au != 0 || bu != 0 {return au * bu, nil
- } else {- return 0, nil
}
+ return 0, nil
case '/':
if bi != 0 {return ai / bi, nil
@@ -1352,14 +1369,14 @@
return af / bf, nil
} else if bu != 0 {return au / bu, nil
- } else {- return nil, errors.New("Can't divide the value by 0")}
+ return nil, errors.New("Can't divide the value by 0")default:
return nil, errors.New("There is no such an operation")}
}
+// Mod returns a % b.
func Mod(a, b interface{}) (int64, error) {av := reflect.ValueOf(a)
bv := reflect.ValueOf(b)
@@ -1386,6 +1403,7 @@
return ai % bi, nil
}
+// ModBool returns the boolean of a % b. If a % b == 0, return true.
func ModBool(a, b interface{}) (bool, error) {res, err := Mod(a, b)
if err != nil {@@ -1394,6 +1412,7 @@
return res == int64(0), nil
}
+// Base64Decode returns the base64 decoding of the given content.
func Base64Decode(content interface{}) (string, error) {conv, err := cast.ToStringE(content)
@@ -1410,6 +1429,7 @@
return string(dec), nil
}
+// Base64Encode returns the base64 encoding of the given content.
func Base64Encode(content interface{}) (string, error) {conv, err := cast.ToStringE(content)
@@ -1420,6 +1440,7 @@
return base64.StdEncoding.EncodeToString([]byte(conv)), nil
}
+// CountWords returns the approximate word count of the given content.
func CountWords(content interface{}) (int, error) {conv, err := cast.ToStringE(content)
@@ -1440,6 +1461,7 @@
return counter, nil
}
+// CountRunes returns the approximate rune count of the given content.
func CountRunes(content interface{}) (int, error) {conv, err := cast.ToStringE(content)
@@ -1457,83 +1479,100 @@
return counter, nil
}
+// Humanize returns the humanized form of a single word.
+// Example: "my-first-post" -> "My first post"
+func Humanize(in interface{}) (string, error) {+ word, err := cast.ToStringE(in)
+ if err != nil {+ return "", err
+ }
+ return inflect.Humanize(word), nil
+}
+
+// Pluralize returns the plural form of a single word.
+func Pluralize(in interface{}) (string, error) {+ word, err := cast.ToStringE(in)
+ if err != nil {+ return "", err
+ }
+ return inflect.Pluralize(word), nil
+}
+
+// Singularize returns the singular form of a single word.
+func Singularize(in interface{}) (string, error) {+ word, err := cast.ToStringE(in)
+ if err != nil {+ return "", err
+ }
+ return inflect.Singularize(word), nil
+}
+
func init() { funcMap = template.FuncMap{- "urlize": helpers.URLize,
- "sanitizeURL": helpers.SanitizeURL,
- "sanitizeurl": helpers.SanitizeURL,
+ "absURL": func(a string) template.HTML { return template.HTML(helpers.AbsURL(a)) },+ "add": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '+') },+ "after": After,
+ "apply": Apply,
+ "base64Decode": Base64Decode,
+ "base64Encode": Base64Encode,
+ "chomp": Chomp,
+ "countrunes": CountRunes,
+ "countwords": CountWords,
+ "dateFormat": DateFormat,
+ "delimit": Delimit,
+ "dict": Dictionary,
+ "div": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '/') },+ "echoParam": ReturnWhenSet,
"eq": Eq,
- "ne": Ne,
- "gt": Gt,
+ "first": First,
"ge": Ge,
- "lt": Lt,
- "le": Le,
- "dict": Dictionary,
+ "getCSV": GetCSV,
+ "getJSON": GetJSON,
+ "getenv": func(varName string) string { return os.Getenv(varName) },+ "gt": Gt,
+ "hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) },+ "highlight": Highlight,
+ "humanize": Humanize,
"in": In,
- "slicestr": Slicestr,
- "substr": Substr,
- "split": Split,
+ "int": func(v interface{}) int { return cast.ToInt(v) },"intersect": Intersect,
"isSet": IsSet,
"isset": IsSet,
- "echoParam": ReturnWhenSet,
- "safeHTML": SafeHTML,
- "safeCSS": SafeCSS,
- "safeJS": SafeJS,
- "safeURL": SafeURL,
- "absURL": func(a string) template.HTML { return template.HTML(helpers.AbsURL(a)) },- "relURL": func(a string) template.HTML { return template.HTML(helpers.RelURL(a)) },- "markdownify": Markdownify,
- "first": First,
"last": Last,
- "after": After,
- "shuffle": Shuffle,
- "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, '-') },- "div": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '/') },+ "le": Le,
+ "lower": func(a string) string { return strings.ToLower(a) },+ "lt": Lt,
+ "markdownify": Markdownify,
"mod": Mod,
- "mul": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '*') },"modBool": ModBool,
- "lower": func(a string) string { return strings.ToLower(a) },- "upper": func(a string) string { return strings.ToUpper(a) },- "title": func(a string) string { return strings.Title(a) },- "hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) },+ "mul": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '*') },+ "ne": Ne,
"partial": Partial,
+ "pluralize": Pluralize,
+ "readDir": ReadDir,
"ref": Ref,
+ "relURL": func(a string) template.HTML { return template.HTML(helpers.RelURL(a)) },"relref": RelRef,
- "apply": Apply,
- "chomp": Chomp,
- "int": func(v interface{}) int { return cast.ToInt(v) },- "string": func(v interface{}) string { return cast.ToString(v) },"replace": Replace,
- "trim": Trim,
- "dateFormat": DateFormat,
- "getJSON": GetJSON,
- "getCSV": GetCSV,
- "readDir": ReadDir,
+ "safeCSS": SafeCSS,
+ "safeHTML": SafeHTML,
+ "safeJS": SafeJS,
+ "safeURL": SafeURL,
+ "sanitizeURL": helpers.SanitizeURL,
+ "sanitizeurl": helpers.SanitizeURL,
"seq": helpers.Seq,
- "getenv": func(varName string) string { return os.Getenv(varName) },- "base64Decode": Base64Decode,
- "base64Encode": Base64Encode,
- "countwords": CountWords,
- "countrunes": CountRunes,
- "pluralize": func(in interface{}) (string, error) {- word, err := cast.ToStringE(in)
- if err != nil {- return "", err
- }
- return inflect.Pluralize(word), nil
- },
- "singularize": func(in interface{}) (string, error) {- word, err := cast.ToStringE(in)
- if err != nil {- return "", err
- }
- return inflect.Singularize(word), nil
- },
+ "shuffle": Shuffle,
+ "singularize": Singularize,
+ "slicestr": Slicestr,
+ "sort": Sort,
+ "split": Split,
+ "string": func(v interface{}) string { return cast.ToString(v) },+ "sub": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '-') },+ "substr": Substr,
+ "title": func(a string) string { return strings.Title(a) },+ "trim": Trim,
+ "upper": func(a string) string { return strings.ToUpper(a) },+ "urlize": helpers.URLize,
+ "where": Where,
}
}
--- a/tpl/template_funcs_test.go
+++ b/tpl/template_funcs_test.go
@@ -19,6 +19,7 @@
"errors"
"fmt"
"github.com/spf13/cast"
+ "github.com/stretchr/testify/assert"
"html/template"
"math/rand"
"path"
@@ -26,8 +27,6 @@
"runtime"
"testing"
"time"
-
- "github.com/stretchr/testify/assert"
)
type tstNoStringer struct {@@ -70,7 +69,6 @@
} {doTestCompare(t, this.tstCompareType, this.funcUnderTest)
}
-
}
func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b interface{}) bool) {@@ -490,7 +488,6 @@
{tstNoStringer{}, 0, 1, false}, {"ĀĀĀ", 0, 1, "Ā"}, // issue #1333 } {-
var result string
if this.v2 == nil {result, err = Slicestr(this.v1)
@@ -618,7 +615,6 @@
}
}
}
-
}
func TestIntersect(t *testing.T) {@@ -1456,7 +1452,6 @@
}
func TestMarkdownify(t *testing.T) {-
result := Markdownify("Hello **World!**") expect := template.HTML("Hello <strong>World!</strong>")@@ -1470,8 +1465,6 @@
strings := []interface{}{"a\n", "b\n"} noStringers := []interface{}{tstNoStringer{}, tstNoStringer{}}- var nilErr *error = nil
-
chomped, _ := Apply(strings, "chomp", ".")
assert.Equal(t, []interface{}{"a", "b"}, chomped)@@ -1486,6 +1479,7 @@
t.Errorf("apply with apply should fail")}
+ var nilErr *error
_, err = Apply(nilErr, "chomp", ".")
if err == nil { t.Errorf("apply with nil in seq should fail")@@ -1505,7 +1499,6 @@
if err == nil { t.Errorf("apply with non-sequence should fail")}
-
}
func TestChomp(t *testing.T) {@@ -1526,6 +1519,22 @@
if err == nil { t.Errorf("Chomp should fail")}
+ }
+}
+
+func TestHumanize(t *testing.T) {+ for _, e := range []struct {+ in, exp string
+ }{+ {"MyCamelPost", "My camel post"},+ {"myLowerCamelPost", "My lower camel post"},+ {"my-dash-post", "My dash post"},+ {"my_underscore_post", "My underscore post"},+ {"posts/my-first-post", "Posts/my first post"},+ } {+ res, err := Humanize(e.in)
+ assert.Nil(t, err)
+ assert.Equal(t, e.exp, res)
}
}
--
⑨