- Split monolithic build-preview.go into modular packages: tools.go (entry), cli/ (list/create/delete/preview commands), config/ (JSON loading), preview/ (rendering engine, funcs, locale) - Extract all template metadata into data/templates_config.json as single source of truth — no hardcoded template data in Go - Replace hand-rolled CLI parsing with github.com/urfave/cli/v2 - Add sensible defaults for --folder (../themes) and --config (./data/templates_config.json) — most commands now run bare - Add type-coercing gt/lt/ge/le template funcs for JSON float64 - Fix AppUrl to gitea.com matching official template convention - rendered.js now exports __REGISTRY__ and __PARAMS__ alongside __RENDERED__ — preview/index.html reads all from single source - Cross-validate all 11 template data contexts against official Gitea source (services/mailer/*.go + templates/mail/*.tmpl) - Update all docs (README, AGENTS, CONTRIBUTING, 5 languages) to reflect new CLI workflow with create scaffolding command Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
160 lines
4.0 KiB
Go
160 lines
4.0 KiB
Go
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{
|
|
"AppUrl": func() string { return "https://gitea.com/" },
|
|
"AppName": func() string { return "Gitea" },
|
|
"DotEscape": func(s string) template.HTML { return template.HTML(template.HTMLEscapeString(s)) },
|
|
"QueryEscape": url.QueryEscape,
|
|
"ShortSha": ShortSha,
|
|
"PathEscapeSegments": PathEscapeSegments,
|
|
"FileSize": FileSize,
|
|
"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, "/")
|
|
}
|
|
|
|
// FileSize formats a byte count as a human-readable string (e.g. "2.5 MB").
|
|
func FileSize(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{"KB", "MB", "GB", "TB"}[exp])
|
|
}
|
|
|
|
// HTMLFormat substitutes %s/%d placeholders with HTML-escaped values.
|
|
func HTMLFormat(fmtStr string, args ...any) template.HTML {
|
|
result := fmtStr
|
|
for i, arg := range args {
|
|
escaped := template.HTMLEscapeString(fmt.Sprint(arg))
|
|
result = strings.Replace(result, fmt.Sprintf("%%[%d]s", i+1), escaped, 1)
|
|
if !strings.Contains(result, fmt.Sprintf("%%[%d]s", i+1)) {
|
|
result = strings.Replace(result, "%s", escaped, 1)
|
|
result = strings.Replace(result, "%d", fmt.Sprint(arg), 1)
|
|
}
|
|
}
|
|
return template.HTML(result)
|
|
}
|
|
|
|
// 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)
|
|
}
|