BrainMinder/internal/funcs/funcs.go

498 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 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
}