error-pages/internal/template/template.go
2024-06-27 19:29:54 +04:00

141 lines
4.2 KiB
Go

package template
import (
"encoding/json"
"fmt"
"html"
"maps"
"os"
"strconv"
"strings"
"text/template"
"time"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/l10n"
)
var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
// current time:
// `{{ now.Unix }}` // `1631610000`
// `{{ now.Hour }}:{{ now.Minute }}:{{ now.Second }}` // `15:4:5`
"now": time.Now,
// current hostname:
// `{{ hostname }}` // `localhost`
"hostname": func() string { h, _ := os.Hostname(); return h }, //nolint:nlreturn
// json-serialized value (safe to use with any type):
// `{{ json "test" }}` // `"test"`
// `{{ json 42 }}` // `42`
"json": func(v any) string { b, _ := json.Marshal(v); return string(b) }, //nolint:nlreturn,errchkjson
// cast any type to int, or return 0 if it's not possible:
// `{{ int "42" }}` // `42`
// `{{ int 42 }}` // `42`
// `{{ int 3.14 }}` // `3`
// `{{ int "test" }}` // `0`
// `{{ int "42test" }}` // `0`
"int": func(v any) int { // cast any type to int, or return 0 if it's not possible
switch v := v.(type) {
case string:
if i, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
return i
}
case int:
return v
case int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
if i, err := strconv.Atoi(fmt.Sprintf("%d", v)); err == nil { // not effective, but safe
return i
}
case float32, float64:
if i, err := strconv.ParseFloat(fmt.Sprintf("%f", v), 32); err == nil { // not effective, but safe
return int(i)
}
case fmt.Stringer:
if i, err := strconv.Atoi(v.String()); err == nil {
return i
}
}
return 0
},
// current application version:
// `{{ version }}` // `1.0.0`
"version": appmeta.Version,
// counts the number of non-overlapping instances of substr in s:
// `{{ strCount "test" "t" }}` // `2`
"strCount": strings.Count,
// reports whether substr is within s:
// `{{ strContains "test" "es" }}` // `true`
// `{{ strContains "test" "ez" }}` // `false`
"strContains": strings.Contains,
// returns a slice of the string s, with all leading and trailing white space removed:
// `{{ strTrimSpace " test " }}` // `test`
"strTrimSpace": strings.TrimSpace,
// returns s without the provided leading prefix string:
// `{{ strTrimPrefix "test" "te" }}` // `st`
"strTrimPrefix": strings.TrimPrefix,
// returns s without the provided trailing suffix string:
// `{{ strTrimSuffix "test" "st" }}` // `te`
"strTrimSuffix": strings.TrimSuffix,
// returns a copy of the string s with all non-overlapping instances of old replaced by new:
// `{{ strReplace "test" "t" "z" }}` // `zesz`
"strReplace": strings.ReplaceAll,
// returns the index of the first instance of substr in s, or -1 if substr is not present in s:
// `{{ strIndex "barfoobaz" "foo" }}` // `3`
"strIndex": strings.Index,
// splits the string s around each instance of one or more consecutive white space characters:
// `{{ strFields "foo bar baz" }}` // `[foo bar baz]`
"strFields": strings.Fields,
// retrieves the value of the environment variable named by the key:
// `{{ env "SHELL" }}` // `/bin/bash`
"env": os.Getenv,
// escapes special characters like "<" to become "&lt;":
// `{{ escape "<test>" }}` // `&lt;test&gt;`
"escape": html.EscapeString,
// returns the content of the JS file with a script for automatic error page localization:
// `{{ l10nScript }}` // `Object.defineProperty(window, ...`
"l10nScript": l10n.L10n,
}
func Render(content string, props Props) (string, error) {
var fns = maps.Clone(builtInFunctions)
maps.Copy(fns, template.FuncMap{ // add custom functions
"hide_details": func() bool { return !props.ShowRequestDetails }, // inverted logic
"l10n_enabled": func() bool { return !props.L10nDisabled }, // inverted logic
})
// allow the direct access to the properties tokens, e.g. `{{ service_port | json }}`
// instead of `{{ .service_port | json }}`
for k, v := range props.Values() {
fns[k] = func() any { return v }
}
tmpl, tErr := template.New("template").Funcs(fns).Parse(content)
if tErr != nil {
return "", fmt.Errorf("failed to parse template: %w", tErr)
}
var buf strings.Builder
if err := tmpl.Execute(&buf, props); err != nil {
return "", err
}
return buf.String(), nil
}