Files
tamabox/internal/template/template.go
T
2026-09-08 10:10:44 +08:00

221 lines
6.4 KiB
Go

// Copyright 2022 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package template
import (
"encoding/json"
"fmt"
"html"
"html/template"
"strconv"
"strings"
"sync"
"time"
"gorm.io/datatypes"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/i18n"
)
var (
funcMap []template.FuncMap
funcMapOnce sync.Once
)
func FuncMap() []template.FuncMap {
funcMapOnce.Do(func() {
funcMap = []template.FuncMap{map[string]interface{}{
"ICP": func() string {
return conf.App.ICP
},
"CommitSHA": func() string {
return conf.BuildCommit
},
"CommitSHAShort": func() string {
if len(conf.BuildCommit) > 7 {
return conf.BuildCommit[:7]
}
return conf.BuildCommit
},
"Date": func(t time.Time, format string) string {
replacer := strings.NewReplacer(datePatterns...)
format = replacer.Replace(format)
return t.Format(format)
},
"QuestionFormat": func(input string) template.HTML {
input = html.EscapeString(input)
input = strings.ReplaceAll(input, "\n", "</br>")
return template.HTML(input)
},
"AnswerFormat": func(input string) template.HTML {
input = html.EscapeString(input)
input = strings.ReplaceAll(input, "\n", "</br>")
return template.HTML(input)
},
// MarkdownFormat 将回答按 Markdown 渲染为安全 HTML(含白名单过滤,防 XSS)。
"MarkdownFormat": MarkdownToHTML,
// MarkdownPlainText 将 Markdown 渲染为纯文本(去标记符号、保留文字与换行、压缩空行),
// 供列表页等不希望展示大字号/彩色排版的场景使用。
"MarkdownPlainText": PlainText,
"SentryDSN": func() string {
return conf.App.SentryDSN
},
"ParsePublicURLs": func(input datatypes.JSON) string {
urls := make(map[string]string)
if err := json.Unmarshal(input, &urls); err != nil {
return ""
}
for _, url := range urls {
return url
}
return ""
},
"ImageBucketCDNHost": func() string {
return conf.Upload.ImageBucketCDNHost
},
"sub": func(a, b int) int {
return a - b
},
// dict 将键值对打包为 map,供 {{template "partial" (dict "Key" val ...)}} 向部分模板传参。
// 参数必须成对且键为字符串,否则返回错误(html/template 会将该错误作为执行错误抛出)。
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, fmt.Errorf("dict: odd number of arguments: %d", len(values))
}
d := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, fmt.Errorf("dict: key at index %d is not a string", i)
}
d[key] = values[i+1]
}
return d, nil
},
"mod": func(a, b int) int {
if b == 0 {
return 0
}
return a % b
},
// loop 生成 1..n 的整数切片,用于模板中按数量 range(如评分 1-10)。
"loop": func(n int) []int {
if n <= 0 {
return []int{}
}
out := make([]int, n)
for i := 1; i <= n; i++ {
out[i-1] = i
}
return out
},
// hasDay 判断逗号分隔的周几列表(0=周日…6=周六,与 time.Weekday 一致)是否包含 wd。
// 用于功能管理页渲染循环定时的开放日复选框。
"hasDay": func(days string, wd int) bool {
for _, part := range strings.Split(days, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
n, err := strconv.Atoi(part)
if err == nil && n == wd {
return true
}
}
return false
},
// parseOptions 解析投票表单字段的 JSON 选项数组(select/radio/checkbox)。
"parseOptions": func(raw string) []string {
if raw == "" {
return []string{}
}
var out []string
if err := json.Unmarshal([]byte(raw), &out); err != nil {
return []string{}
}
return out
},
// percentOf 计算百分比(模板内无法做除法)。
"percentOf": func(part, total int64) int {
if total <= 0 {
return 0
}
return int(part * 100 / total)
},
// add 加法(模板序号 1-based 等场景)。
"add": func(a, b int) int {
return a + b
},
// fieldTypeLabel 返回表单字段类型的本地化名称(参与端题型标签)。
"fieldTypeLabel": func(lang, fieldType string) string {
key := map[string]string{
"text": "vote.fieldTypeText",
"textarea": "vote.fieldTypeTextarea",
"select": "vote.fieldTypeSelect",
"radio": "vote.fieldTypeRadio",
"checkbox": "vote.fieldTypeCheckbox",
"number": "vote.fieldTypeNumber",
"rating": "vote.fieldTypeRating",
"rank": "vote.fieldTypeRank",
}[fieldType]
if key == "" {
return ""
}
return i18n.T(lang, key)
},
// fieldTypeLabelClass 返回表单字段类型的标签配色(题型视觉区分)。
"fieldTypeLabelClass": func(fieldType string) string {
switch fieldType {
case "text", "textarea":
return "" // 默认灰
case "radio", "select":
return "uk-label-primary"
case "checkbox":
return "uk-label-success"
case "rating":
return "uk-label-warning"
case "rank":
return "uk-label-danger"
case "number":
return "uk-label-secondary"
default:
return ""
}
},
// toJSON 将值序列化为 JSON 并标记为安全的 JS 字面量。
// 必须用于 <script> 上下文中的 JSON 注入:template.HTML 会在 script 上下文中
// 被 html/template 按 JS 规则转义(引号被转义导致 JS 语法错误/值变成字符串),
// template.JS 则原样输出。
"toJSON": func(v interface{}) template.JS {
b, err := json.Marshal(v)
if err != nil {
return template.JS("[]")
}
return template.JS(b)
},
"Safe": Safe,
// 兼容小写别名:模板中历史遗留 {{safe ...}} 调用
"safe": Safe,
// i18n 模板函数:
// 模板用法:{{t .LangCode "key"}} 或 {{t .LangCode "key" .Arg}}
// 语言名:{{langName "zh-CN"}} → 简体中文
"t": func(lang, key string, args ...interface{}) string {
return i18n.T(lang, key, args...)
},
"langName": func(lang string) string {
return i18n.Name(lang)
},
}}
})
return funcMap
}
func Safe(raw string) template.HTML {
return template.HTML(raw)
}