// 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", "
") return template.HTML(input) }, "AnswerFormat": func(input string) template.HTML { input = html.EscapeString(input) input = strings.ReplaceAll(input, "\n", "
") 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 字面量。 // 必须用于