diff --git a/docs/custom-api.md b/docs/custom-api.md index 9848276..7eb6ca6 100644 --- a/docs/custom-api.md +++ b/docs/custom-api.md @@ -325,13 +325,24 @@ The following helper functions provided by Glance are available: - `toFloat(i int) float`: Converts an integer to a float. - `toInt(f float) int`: Converts a float to an integer. - `toRelativeTime(t time.Time) template.HTMLAttr`: Converts Time to a relative time such as 2h, 1d, etc which dynamically updates. **NOTE:** the value of this function should be used as an attribute in an HTML tag, e.g. ``. -- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "RFC3339", "RFC3339Nano", "DateTime", "DateOnly", "TimeOnly". +- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly". +- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`. - `add(a, b float) float`: Adds two numbers. - `sub(a, b float) float`: Subtracts two numbers. - `mul(a, b float) float`: Multiplies two numbers. - `div(a, b float) float`: Divides two numbers. - `formatApproxNumber(n int) string`: Formats a number to be more human-readable, e.g. 1000 -> 1k. - `formatNumber(n float|int) string`: Formats a number with commas, e.g. 1000 -> 1,000. +- `trimPrefix(prefix string, str string) string`: Trims the prefix from a string. +- `trimSuffix(suffix string, str string) string`: Trims the suffix from a string. +- `trimSpace(str string) string`: Trims whitespace from a string on both ends. +- `replaceAll(old string, new string, str string) string`: Replaces all occurrences of a string in a string. +- `findMatch(pattern string, str string) string`: Finds the first match of a regular expression in a string. +- `findSubmatch(pattern string, str string) string`: Finds the first submatch of a regular expression in a string. +- `sortByString(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a string key in either ascending or descending order. +- `sortByInt(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by an integer key in either ascending or descending order. +- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order. +- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). The following helper functions provided by Go's `text/template` are available: diff --git a/internal/glance/widget-custom-api.go b/internal/glance/widget-custom-api.go index e20ff5a..ec88016 100644 --- a/internal/glance/widget-custom-api.go +++ b/internal/glance/widget-custom-api.go @@ -11,6 +11,9 @@ import ( "log/slog" "math" "net/http" + "regexp" + "sort" + "strconv" "strings" "sync" "time" @@ -340,6 +343,22 @@ func (r *decoratedGJSONResult) Bool(key string) bool { } var customAPITemplateFuncs = func() template.FuncMap { + var regexpCacheMu sync.Mutex + var regexpCache = make(map[string]*regexp.Regexp) + + getCachedRegexp := func(pattern string) *regexp.Regexp { + regexpCacheMu.Lock() + defer regexpCacheMu.Unlock() + + regex, exists := regexpCache[pattern] + if !exists { + regex = regexp.MustCompile(pattern) + regexpCache[pattern] = regex + } + + return regex + } + funcs := template.FuncMap{ "toFloat": func(a int) float64 { return float64(a) @@ -369,6 +388,83 @@ var customAPITemplateFuncs = func() template.FuncMap { // Shorthand to do both of the above with a single function call return dynamicRelativeTimeAttrs(customAPIFuncParseTime(layout, value)) }, + // The reason we flip the parameter order is so that you can chain multiple calls together like this: + // {{ .JSON.String "foo" | trimPrefix "bar" | doSomethingElse }} + // instead of doing this: + // {{ trimPrefix (.JSON.String "foo") "bar" | doSomethingElse }} + // since the piped value gets passed as the last argument to the function. + "trimPrefix": func(prefix, s string) string { + return strings.TrimPrefix(s, prefix) + }, + "trimSuffix": func(suffix, s string) string { + return strings.TrimSuffix(s, suffix) + }, + "trimSpace": strings.TrimSpace, + "replaceAll": func(old, new, s string) string { + return strings.ReplaceAll(s, old, new) + }, + "findMatch": func(pattern, s string) string { + if s == "" { + return "" + } + + return getCachedRegexp(pattern).FindString(s) + }, + "findSubmatch": func(pattern, s string) string { + if s == "" { + return "" + } + + regex := getCachedRegexp(pattern) + return itemAtIndexOrDefault(regex.FindStringSubmatch(s), 1, "") + }, + "sortByString": func(key, order string, results []decoratedGJSONResult) []decoratedGJSONResult { + sort.Slice(results, func(a, b int) bool { + if order == "asc" { + return results[a].String(key) < results[b].String(key) + } + + return results[a].String(key) > results[b].String(key) + }) + + return results + }, + "sortByInt": func(key, order string, results []decoratedGJSONResult) []decoratedGJSONResult { + sort.Slice(results, func(a, b int) bool { + if order == "asc" { + return results[a].Int(key) < results[b].Int(key) + } + + return results[a].Int(key) > results[b].Int(key) + }) + + return results + }, + "sortByFloat": func(key, order string, results []decoratedGJSONResult) []decoratedGJSONResult { + sort.Slice(results, func(a, b int) bool { + if order == "asc" { + return results[a].Float(key) < results[b].Float(key) + } + + return results[a].Float(key) > results[b].Float(key) + }) + + return results + }, + "sortByTime": func(key, layout, order string, results []decoratedGJSONResult) []decoratedGJSONResult { + sort.Slice(results, func(a, b int) bool { + timeA := customAPIFuncParseTime(layout, results[a].String(key)) + timeB := customAPIFuncParseTime(layout, results[b].String(key)) + + if order == "asc" { + return timeA.Before(timeB) + } + + return timeA.After(timeB) + }) + + return results + }, } for key, value := range globalTemplateFunctions { @@ -382,6 +478,13 @@ var customAPITemplateFuncs = func() template.FuncMap { func customAPIFuncParseTime(layout, value string) time.Time { switch strings.ToLower(layout) { + case "unix": + asInt, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return time.Unix(0, 0) + } + + return time.Unix(asInt, 0) case "rfc3339": layout = time.RFC3339 case "rfc3339nano": @@ -390,8 +493,6 @@ func customAPIFuncParseTime(layout, value string) time.Time { layout = time.DateTime case "dateonly": layout = time.DateOnly - case "timeonly": - layout = time.TimeOnly } parsed, err := time.Parse(layout, value)