From 6891f7f421eac71dac8f8687255ede5189e7eb3a Mon Sep 17 00:00:00 2001 From: Aleks Date: Wed, 25 May 2022 01:47:08 +0200 Subject: [PATCH] templates: Add `humanize` function (#4767) Co-authored-by: Francis Lavoie --- modules/caddyhttp/templates/templates.go | 23 +++++++++ modules/caddyhttp/templates/tplcontext.go | 40 +++++++++++++++ .../caddyhttp/templates/tplcontext_test.go | 49 +++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index b2fe184d..85612ba0 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -238,6 +238,29 @@ func init() { // {{stripHTML "Shows only text content"}} // ``` // +// ##### `humanize` +// +// Transforms size and time inputs to a human readable format. +// This uses the [go-humanize](https://github.com/dustin/go-humanize) library. +// +// The first argument must be a format type, and the last argument +// is the input, or the input can be piped in. The supported format +// types are: +// - **size** which turns an integer amount of bytes into a string like `2.3 MB` +// - **time** which turns a time string into a relative time string like `2 weeks ago` +// +// For the `time` format, the layout for parsing the input can be configured +// by appending a colon `:` followed by the desired time layout. You can +// find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants). +// The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`. +// +// ``` +// {{humanize "size" "2048000"}} +// {{placeholder "http.response.header.Content-Length" | humanize "size"}} +// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}} +// {{humanize "time:2006-Jan-02" "2022-May-05"}} +// ``` + type Templates struct { // The root path from which to load files. Required if template functions // accessing the file system are used (such as include). Default is diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 7843455a..bae24ba8 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -26,11 +26,13 @@ import ( "strings" "sync" "text/template" + "time" "github.com/Masterminds/sprig/v3" "github.com/alecthomas/chroma/formatters/html" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/dustin/go-humanize" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting" "github.com/yuin/goldmark/extension" @@ -81,6 +83,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template { "placeholder": c.funcPlaceholder, "fileExists": c.funcFileExists, "httpError": c.funcHTTPError, + "humanize": c.funcHumanize, }) return c.tpl } @@ -398,6 +401,43 @@ func (c TemplateContext) funcHTTPError(statusCode int) (bool, error) { return false, caddyhttp.Error(statusCode, nil) } +// funcHumanize transforms size and time inputs to a human readable format. +// +// Size inputs are expected to be integers, and are formatted as a +// byte size, such as "83 MB". +// +// Time inputs are parsed using the given layout (default layout is RFC1123Z) +// and are formatted as a relative time, such as "2 weeks ago". +// See https://pkg.go.dev/time#pkg-constants for time layout docs. +func (c TemplateContext) funcHumanize(formatType, data string) (string, error) { + // The format type can optionally be followed + // by a colon to provide arguments for the format + parts := strings.Split(formatType, ":") + + switch parts[0] { + case "size": + dataint, dataerr := strconv.ParseUint(data, 10, 64) + if dataerr != nil { + return "", fmt.Errorf("humanize: size cannot be parsed: %s", dataerr.Error()) + } + return humanize.Bytes(dataint), nil + + case "time": + timelayout := time.RFC1123Z + if len(parts) > 1 { + timelayout = parts[1] + } + + dataint, dataerr := time.Parse(timelayout, data) + if dataerr != nil { + return "", fmt.Errorf("humanize: time cannot be parsed: %s", dataerr.Error()) + } + return humanize.Time(dataint), nil + } + + return "", fmt.Errorf("no know function was given") +} + // WrappedHeader wraps niladic functions so that they // can be used in templates. (Template functions must // return a value.) diff --git a/modules/caddyhttp/templates/tplcontext_test.go b/modules/caddyhttp/templates/tplcontext_test.go index 61fc80c6..15a369ee 100644 --- a/modules/caddyhttp/templates/tplcontext_test.go +++ b/modules/caddyhttp/templates/tplcontext_test.go @@ -606,6 +606,55 @@ title = "Welcome" } +func TestHumanize(t *testing.T) { + tplContext := getContextOrFail(t) + for i, test := range []struct { + format string + inputData string + expect string + errorCase bool + verifyErr func(actual_string, substring string) bool + }{ + { + format: "size", + inputData: "2048000", + expect: "2.0 MB", + errorCase: false, + verifyErr: strings.Contains, + }, + { + format: "time", + inputData: "Fri, 05 May 2022 15:04:05 +0200", + expect: "ago", + errorCase: false, + verifyErr: strings.HasSuffix, + }, + { + format: "time:2006-Jan-02", + inputData: "2022-May-05", + expect: "ago", + errorCase: false, + verifyErr: strings.HasSuffix, + }, + { + format: "time", + inputData: "Fri, 05 May 2022 15:04:05 GMT+0200", + expect: "error:", + errorCase: true, + verifyErr: strings.HasPrefix, + }, + } { + if actual, err := tplContext.funcHumanize(test.format, test.inputData); !test.verifyErr(actual, test.expect) { + if !test.errorCase { + t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, actual) + if err != nil { + t.Errorf("Test %d: error: %s", i, err.Error()) + } + } + } + } +} + func getContextOrFail(t *testing.T) TemplateContext { tplContext, err := initTestContext() t.Cleanup(func() {