package funcs import ( "bytes" "fmt" "html/template" "math" "net/url" "strconv" "strings" "time" "unicode" "golang.org/x/text/language" "golang.org/x/text/message" ) var printer = message.NewPrinter(language.English) var TemplateFuncs = template.FuncMap{ // Time functions "now": time.Now, "timeSince": time.Since, "timeUntil": time.Until, "formatTime": formatTime, "approxDuration": approxDuration, // String functions "uppercase": strings.ToUpper, "lowercase": strings.ToLower, "pluralize": pluralize, "slugify": slugify, "safeHTML": safeHTML, // Slice functions "join": strings.Join, // Number functions "incr": incr, "decr": decr, "formatInt": formatInt, "formatFloat": formatFloat, // Boolean functions "yesno": yesno, // URL functions "urlSetParam": urlSetParam, "urlDelParam": urlDelParam, } func formatTime(format string, t time.Time) string { return t.Format(format) } func approxDuration(d time.Duration) string { const ( day = 24 * time.Hour year = 365 * day ) formatUnit := func(count int, singular, plural string) string { if count == 1 { return fmt.Sprintf("1 %s", singular) } return fmt.Sprintf("%d %s", count, plural) } switch { case d >= year: return formatUnit(int(math.Round(float64(d)/float64(year))), "year", "years") case d >= day: return formatUnit(int(math.Round(float64(d)/float64(day))), "day", "days") case d >= time.Hour: return formatUnit(int(math.Round(d.Hours())), "hour", "hours") case d >= time.Minute: return formatUnit(int(math.Round(d.Minutes())), "minute", "minutes") case d >= time.Second: return formatUnit(int(math.Round(d.Seconds())), "second", "seconds") default: return "less than 1 second" } } func pluralize(count any, singular string, plural string) (string, error) { n, err := toInt64(count) if err != nil { return "", err } if n == 1 { return singular, nil } return plural, nil } func slugify(s string) string { var buf bytes.Buffer for _, r := range s { switch { case r > unicode.MaxASCII: continue case unicode.IsLetter(r): buf.WriteRune(unicode.ToLower(r)) case unicode.IsDigit(r), r == '_', r == '-': buf.WriteRune(r) case unicode.IsSpace(r): buf.WriteRune('-') } } return buf.String() } func safeHTML(s string) template.HTML { return template.HTML(s) } func incr(i any) (int64, error) { n, err := toInt64(i) if err != nil { return 0, err } n++ return n, nil } func decr(i any) (int64, error) { n, err := toInt64(i) if err != nil { return 0, err } n-- return n, nil } func formatInt(i any) (string, error) { n, err := toInt64(i) if err != nil { return "", err } return printer.Sprintf("%d", n), nil } func formatFloat(f float64, dp int) string { format := "%." + strconv.Itoa(dp) + "f" return printer.Sprintf(format, f) } func yesno(b bool) string { if b { return "Yes" } return "No" } func urlSetParam(u *url.URL, key string, value any) *url.URL { nu := *u values := nu.Query() values.Set(key, fmt.Sprintf("%v", value)) nu.RawQuery = values.Encode() return &nu } func urlDelParam(u *url.URL, key string) *url.URL { nu := *u values := nu.Query() values.Del(key) nu.RawQuery = values.Encode() return &nu } func toInt64(i any) (int64, error) { switch v := i.(type) { case int: return int64(v), nil case int8: return int64(v), nil case int16: return int64(v), nil case int32: return int64(v), nil case int64: return v, nil case uint: return int64(v), nil case uint8: return int64(v), nil case uint16: return int64(v), nil case uint32: return int64(v), nil // Note: uint64 not supported due to risk of truncation. case string: return strconv.ParseInt(v, 10, 64) } return 0, fmt.Errorf("unable to convert type %T to int", i) }