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("%v", 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, "w3-input w3-border") } 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(``, name, label) } o = o + fmt.Sprintf(`` 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 + "
" if len(label) > 0 { o = o + fmt.Sprintf(``, 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 + "

" o = o + fmt.Sprintf(``, id_str, name, checked, option.Key, attributes) o = o + fmt.Sprintf(``, id_str, option.Value) o = o + "

" } o = o + "" 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(``, name, label) } o = o + fmt.Sprintf(``, 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(``, name, label) } o = o + fmt.Sprintf(``, 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 }