215 lines
6.7 KiB
Go
215 lines
6.7 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"
|
||
"github.com/tamakyi/TamaBox/internal/security/moderation"
|
||
)
|
||
|
||
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)
|
||
},
|
||
// 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)
|
||
},
|
||
|
||
// moderationStrictness 返回账号「最终生效」的审核严格度档位(lax / standard / strict)。
|
||
//
|
||
// 设置页下拉框必须用它回显,不能直接用 LoggedUser.ModerationStrictness:
|
||
// 存量账号该字段为空值(表示"跟随站点默认"),三个 <option> 的 eq 判定全部为假,
|
||
// 浏览器会默认选中第一个选项(宽松),而实际生效的是站点默认档位(通常为严格)——
|
||
// 界面会告诉箱主"你是宽松",与真实拦截行为相反。
|
||
// 走 moderation.StrictnessFor 解析,保证回显与 internal/security/censor/policy.go
|
||
// 的裁决口径是同一份事实来源(含站点默认与非法值回退)。
|
||
"moderationStrictness": func(raw string) string {
|
||
return string(moderation.StrictnessFor(raw))
|
||
},
|
||
}}
|
||
})
|
||
return funcMap
|
||
}
|
||
|
||
func Safe(raw string) template.HTML {
|
||
return template.HTML(raw)
|
||
}
|