499 lines
11 KiB
Go
499 lines
11 KiB
Go
package funcs
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"math"
|
|
"net/url"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/yuin/goldmark"
|
|
highlighting "github.com/yuin/goldmark-highlighting"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
)
|
|
|
|
type WidgetOption struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
const (
|
|
day = 24 * time.Hour
|
|
year = 365 * day
|
|
)
|
|
|
|
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,
|
|
"renderFieldValue": renderFieldValue,
|
|
"renderFieldValues": renderFieldValues,
|
|
"stringToArray": stringToArray,
|
|
|
|
// Slice functions
|
|
"join": strings.Join,
|
|
|
|
// Number functions
|
|
"incr": incr,
|
|
"decr": decr,
|
|
"addI": addI,
|
|
"subI": subI,
|
|
"formatInt": formatInt,
|
|
"formatFloat": formatFloat,
|
|
|
|
// Boolean functions
|
|
"yesno": yesno,
|
|
|
|
// URL functions
|
|
"urlSetParam": urlSetParam,
|
|
"urlDelParam": urlDelParam,
|
|
|
|
//Markdown render
|
|
"markdownfy": markdownfy,
|
|
|
|
"linksList": linksList,
|
|
|
|
//Widgets
|
|
"widget_relation_type": widget_relation_type,
|
|
"widget_text": widget_text,
|
|
"widget_select": widget_select,
|
|
"widget_slim_select": widget_slim_select,
|
|
"widget_checkboxes": widget_checkboxes,
|
|
"field_widget": field_widget,
|
|
|
|
//Map
|
|
"map": tmap,
|
|
}
|
|
|
|
func formatTime(format string, t time.Time) string {
|
|
return t.Format(format)
|
|
}
|
|
|
|
func approxDuration(d time.Duration) string {
|
|
if d < time.Second {
|
|
return "less than 1 second"
|
|
}
|
|
|
|
ds := int(math.Round(d.Seconds()))
|
|
if ds == 1 {
|
|
return "1 second"
|
|
} else if ds < 60 {
|
|
return fmt.Sprintf("%d seconds", ds)
|
|
}
|
|
|
|
dm := int(math.Round(d.Minutes()))
|
|
if dm == 1 {
|
|
return "1 minute"
|
|
} else if dm < 60 {
|
|
return fmt.Sprintf("%d minutes", dm)
|
|
}
|
|
|
|
dh := int(math.Round(d.Hours()))
|
|
if dh == 1 {
|
|
return "1 hour"
|
|
} else if dh < 24 {
|
|
return fmt.Sprintf("%d hours", dh)
|
|
}
|
|
|
|
dd := int(math.Round(float64(d / day)))
|
|
if dd == 1 {
|
|
return "1 day"
|
|
} else if dd < 365 {
|
|
return fmt.Sprintf("%d days", dd)
|
|
}
|
|
|
|
dy := int(math.Round(float64(d / year)))
|
|
if dy == 1 {
|
|
return "1 year"
|
|
}
|
|
|
|
return fmt.Sprintf("%d years", dy)
|
|
}
|
|
|
|
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 addI(i1 any, i2 any) (int64, error) {
|
|
i1_64, err := toInt64(i1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
i2_64, err := toInt64(i2)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
t := i1_64 + i2_64
|
|
return t, nil
|
|
}
|
|
|
|
func subI(i1 any, i2 any) (int64, error) {
|
|
i1_64, err := toInt64(i1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
i2_64, err := toInt64(i2)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
t := i1_64 - i2_64
|
|
return t, 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)
|
|
}
|
|
|
|
func markdownfy(s string) string {
|
|
markdown := goldmark.New(
|
|
goldmark.WithExtensions(
|
|
highlighting.Highlighting,
|
|
),
|
|
)
|
|
|
|
var buf bytes.Buffer
|
|
markdown.Convert([]byte(s), &buf)
|
|
return buf.String()
|
|
}
|
|
|
|
func stringToArray(s string, delim string) []string {
|
|
return strings.Split(strings.Trim(s, delim), delim)
|
|
}
|
|
|
|
func renderFieldValueAsString(value string, widget string) string {
|
|
o := ""
|
|
widget = strings.ToLower(widget)
|
|
switch widget {
|
|
case "url":
|
|
if len(value) > 0 {
|
|
label, _ := strings.CutPrefix(value, "https://")
|
|
label, _ = strings.CutPrefix(label, "www.")
|
|
if len(label) > 20 {
|
|
label = label[:20] + " ..."
|
|
}
|
|
o = fmt.Sprintf("<a href=\"%v\" title=\"%v\" target=\"_new\">%v</a>", value, value, label)
|
|
}
|
|
default:
|
|
o = strings.Trim(fmt.Sprint(value), "[]")
|
|
}
|
|
return o
|
|
}
|
|
|
|
func renderFieldValue(value string, widget string) template.HTML {
|
|
return template.HTML(renderFieldValueAsString(value, widget))
|
|
}
|
|
|
|
func renderFieldValues(values map[int]string, widget string) template.HTML {
|
|
if len(values) > 0 {
|
|
var values_a []string
|
|
for _, value := range values {
|
|
values_a = append(values_a, renderFieldValueAsString(value, widget))
|
|
}
|
|
|
|
return template.HTML(strings.Join(values_a, ","))
|
|
}
|
|
|
|
return template.HTML("")
|
|
}
|
|
|
|
func linksList(urls map[string]string, delim string) template.HTML {
|
|
o := ""
|
|
return template.HTML(o)
|
|
}
|
|
|
|
func widget_relation_type(name string, value string, attributes string) template.HTML {
|
|
var options []WidgetOption
|
|
|
|
options = append(options, WidgetOption{Key: "Parent", Value: "Parent"})
|
|
options = append(options, WidgetOption{Key: "Child", Value: "Child"})
|
|
options = append(options, WidgetOption{Key: "Link", Value: "Link"})
|
|
|
|
return widget_select(name, "", value, options, attributes)
|
|
}
|
|
|
|
func widget_select(name string, label string, value any, options []WidgetOption, attributes string) template.HTML {
|
|
return render_select(name, label, value, options, attributes, "")
|
|
}
|
|
|
|
func widget_slim_select(name string, label string, value any, options []WidgetOption, attributes string) template.HTML {
|
|
return render_select(name, label, value, options, attributes, "slim-select w3-border")
|
|
}
|
|
|
|
func render_select(name string, label string, value any, options []WidgetOption, attributes string, classes string) template.HTML {
|
|
var values []string
|
|
|
|
switch v := value.(type) {
|
|
case int64:
|
|
values = append(values, strconv.FormatInt(v, 10))
|
|
case []int64:
|
|
for _, i := range v {
|
|
values = append(values, strconv.FormatInt(i, 10))
|
|
}
|
|
case string:
|
|
if strings.HasPrefix(v, "|") && strings.HasSuffix(v, "|") {
|
|
values = strings.Split(strings.Trim(v, "|"), "|")
|
|
} else {
|
|
values = append(values, v)
|
|
}
|
|
case []string:
|
|
values = v
|
|
}
|
|
|
|
o := ""
|
|
if len(label) > 0 {
|
|
o = fmt.Sprintf(`<label for="%v">%v</label>`, name, label)
|
|
}
|
|
o = o + fmt.Sprintf(`<select name="%v" id="%v" class="%v" %v>`, name, name, classes, attributes)
|
|
selected := ""
|
|
for _, option := range options {
|
|
selected = ""
|
|
if slices.Contains(values, option.Key) {
|
|
selected = "selected"
|
|
}
|
|
o = o + fmt.Sprintf(`<option value="%v" %v>%v</option>`, option.Key, selected, option.Value)
|
|
}
|
|
o = o + `</select>`
|
|
return template.HTML(o)
|
|
}
|
|
|
|
func widget_checkboxes(name string, label string, value any, options []WidgetOption, attributes string) template.HTML {
|
|
var values []string
|
|
|
|
switch v := value.(type) {
|
|
case int64:
|
|
values = append(values, strconv.FormatInt(v, 10))
|
|
case []int64:
|
|
for _, i := range v {
|
|
values = append(values, strconv.FormatInt(i, 10))
|
|
}
|
|
case string:
|
|
if strings.HasPrefix(v, "|") && strings.HasSuffix(v, "|") {
|
|
values = strings.Split(strings.Trim(v, "|"), "|")
|
|
} else {
|
|
values = append(values, v)
|
|
}
|
|
case []string:
|
|
values = v
|
|
}
|
|
|
|
o := ""
|
|
o = o + "<fieldset>"
|
|
if len(label) > 0 {
|
|
o = o + fmt.Sprintf(`<label>%v</label>`, label)
|
|
}
|
|
checked := ""
|
|
for _, option := range options {
|
|
checked = ""
|
|
if slices.Contains(values, option.Key) {
|
|
checked = `checked="checked"`
|
|
}
|
|
id_str := strings.ReplaceAll(name+"-"+option.Key, " ", "-")
|
|
o = o + "<p>"
|
|
//o = o + fmt.Sprintf(`<label class="switch"><input id="%v" type="checkbox" name="%v" %v value="%v" %v /><span class="slider round"></span></label>`, id_str, name, checked, option.Key, attributes)
|
|
o = o + fmt.Sprintf(`<label><input id="%v" type="checkbox" name="%v" %v value="%v" %v />%v</label>`, id_str, name, checked, option.Key, attributes, option.Value)
|
|
//o = o + fmt.Sprintf(`<label class="label-checkbox" for="%v">%v</label>`, id_str, option.Value)
|
|
o = o + "</p>"
|
|
}
|
|
o = o + "</fiedlset>"
|
|
return template.HTML(o)
|
|
}
|
|
|
|
func widget_text(name string, label string, value string, attributes string) template.HTML {
|
|
o := ""
|
|
if len(label) > 0 {
|
|
o = o + fmt.Sprintf(`<label for="%v">%v</label>`, name, label)
|
|
}
|
|
o = o + fmt.Sprintf(`<input id="%v" type="text" name="%v" value="%v" %v />`, name, name, value, attributes)
|
|
return template.HTML(o)
|
|
}
|
|
|
|
func widget_url(name string, label string, value string, attributes string) template.HTML {
|
|
o := ""
|
|
if len(label) > 0 {
|
|
o = o + fmt.Sprintf(`<label for="%v">%v</label>`, name, label)
|
|
}
|
|
o = o + fmt.Sprintf(`<input id="%v" type="url" name="%v" value="%v" %v />`, name, name, value, attributes)
|
|
return template.HTML(o)
|
|
}
|
|
|
|
func field_widget(widget string, bm_type_field_id int64, counter int, label string, value string, valid_values string, attributes string) template.HTML {
|
|
widget_name := fmt.Sprintf("FieldsValues-%v-%v", bm_type_field_id, counter)
|
|
var options []WidgetOption
|
|
|
|
switch widget {
|
|
case "select":
|
|
entries := strings.Split(valid_values, "|")
|
|
for _, entry := range entries {
|
|
parts := strings.Split(entry, ",")
|
|
if len(parts) == 2 {
|
|
options = append(options, WidgetOption{Key: parts[0], Value: parts[1]})
|
|
} else if len(parts) == 1 {
|
|
options = append(options, WidgetOption{Key: parts[0], Value: parts[0]})
|
|
}
|
|
}
|
|
return widget_select(widget_name, label, value, options, attributes)
|
|
case "text":
|
|
return widget_text(widget_name, label, value, attributes)
|
|
case "url":
|
|
return widget_url(widget_name, label, value, attributes)
|
|
}
|
|
|
|
return template.HTML("")
|
|
}
|
|
|
|
func tmap(pairs ...any) (map[string]any, error) {
|
|
if len(pairs)%2 != 0 {
|
|
return nil, errors.New("misaligned map")
|
|
}
|
|
|
|
m := make(map[string]any, len(pairs)/2)
|
|
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
key, ok := pairs[i].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot use type %T as map key", pairs[i])
|
|
}
|
|
m[key] = pairs[i+1]
|
|
}
|
|
return m, nil
|
|
}
|