Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
221 lines
6.4 KiB
Go
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)
|
|
}
|