package preview import ( "fmt" "html/template" "net/url" "strings" ) // TemplateFuncs maps Gitea's built-in template functions available in mail templates. var TemplateFuncs = template.FuncMap{ "dict": Dict, "AppUrl": func() string { return "https://gitea.com/" }, "AppName": func() string { return "Gitea" }, "AppDomain": func() string { return "gitea.com" }, "DotEscape": func(s string) string { return strings.ReplaceAll(s, ".", "\u200d.\u200d") }, "QueryEscape": url.QueryEscape, "ShortSha": ShortSha, "PathEscapeSegments": PathEscapeSegments, "FormatByteSize": previewFormatByteSize, "HTMLFormat": HTMLFormat, "Dict": Dict, "gt": Gt, // override built-in for JSON float64/int coercion "lt": Lt, "ge": Ge, "le": Le, } // ShortSha truncates a commit SHA to 10 characters. func ShortSha(s string) string { if len(s) > 10 { return s[:10] } return s } // PathEscapeSegments splits a path and URL-escapes each segment. func PathEscapeSegments(s string) string { parts := strings.Split(s, "/") for i, p := range parts { parts[i] = url.PathEscape(p) } return strings.Join(parts, "/") } // previewFormatByteSize converts JSON fixture numbers to Gitea's int64 input type. func previewFormatByteSize(size any) (string, error) { switch n := size.(type) { case int64: return FormatByteSize(n), nil case float64: return FormatByteSize(int64(n)), nil default: return "", fmt.Errorf("FormatByteSize: unsupported size type %T", size) } } // FormatByteSize mirrors Gitea 28's IEC attachment size formatting. func FormatByteSize(n int64) string { const unit = 1024 if n < unit { return fmt.Sprintf("%d B", n) } div, exp := int64(unit), 0 for nn := n / unit; nn >= unit && exp < 3; nn /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %s", float64(n)/float64(div), []string{"KiB", "MiB", "GiB", "TiB"}[exp]) } // HTMLFormat substitutes %s/%d placeholders with HTML-escaped values. func HTMLFormat(fmtStr string, args ...any) template.HTML { return template.HTML(fmt.Sprintf(fmtStr, escapedArgs(args)...)) } // Dict builds a map from alternating key/value pairs (used by Gitea's Dict function). func Dict(values ...any) (map[string]any, error) { if len(values)%2 != 0 { return nil, fmt.Errorf("odd number of Dict args") } dict := make(map[string]any, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, fmt.Errorf("Dict keys must be strings") } dict[key] = values[i+1] } return dict, nil } // toFloat64 coerces a value to float64 if numeric. func toFloat64(v any) (float64, bool) { switch val := v.(type) { case float64: return val, true case float32: return float64(val), true case int: return float64(val), true case int64: return float64(val), true case int32: return float64(val), true case int16: return float64(val), true case int8: return float64(val), true case uint: return float64(val), true case uint64: return float64(val), true case uint32: return float64(val), true case uint16: return float64(val), true case uint8: return float64(val), true default: return 0, false } } // Gt is a type-coercing > comparison that handles JSON float64 vs int literals. func Gt(a, b any) bool { af, aok := toFloat64(a) bf, bok := toFloat64(b) if aok && bok { return af > bf } // Fallback to string comparison return fmt.Sprint(a) > fmt.Sprint(b) } // Lt is a type-coercing < comparison that handles JSON float64 vs int literals. func Lt(a, b any) bool { af, aok := toFloat64(a) bf, bok := toFloat64(b) if aok && bok { return af < bf } return fmt.Sprint(a) < fmt.Sprint(b) } // Ge is a type-coercing >= comparison. func Ge(a, b any) bool { af, aok := toFloat64(a) bf, bok := toFloat64(b) if aok && bok { return af >= bf } return fmt.Sprint(a) >= fmt.Sprint(b) } // Le is a type-coercing <= comparison. func Le(a, b any) bool { af, aok := toFloat64(a) bf, bok := toFloat64(b) if aok && bok { return af <= bf } return fmt.Sprint(a) <= fmt.Sprint(b) }