/
html.go
137 lines (118 loc) · 3.42 KB
/
html.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// SPDX-FileCopyrightText: 2018-2024 caixw
//
// SPDX-License-Identifier: MIT
// Package html 提供输出 HTML 内容的解码函数
//
// srv := server.New("", "", &server.Options{
// Codec: web.New().AddMimetype("text/html", html.Marshal, html.Unmarshal, "")
// })
// tpl := template.ParseFiles(...)
//
// func handle(ctx *web.Context) Responser {
// obj := &struct{
// XMLName struct{} `html:"Object"`
// Data string
// }{}
// return Object(200, obj, nil)
// }
//
// 预定义的模板
//
// 框架本身提供了一些数据类型的定义,比如 [web.Problem],
// 用户需要提供由 [web.Problem.MarshalHTML] 返回的模板定义。
// 如果用户还使用了 [server.RenderResponse],那么也需要提供对应的模板定义。
package html
import (
"bytes"
"fmt"
"html/template"
"io"
"reflect"
"github.com/issue9/mux/v8/header"
"golang.org/x/text/message"
"github.com/issue9/web"
"github.com/issue9/web/mimetype"
)
const Mimetype = header.HTML
// Marshaler 自定义 HTML 输出需要实现的接口
//
// 当前接口仅适用于由 [InstallView] 和 [InstallLocaleView] 管理的模板。
type Marshaler interface {
// MarshalHTML 将对象转换成可用于模板的对象结构
//
// name 表示模板名称;
// data 表示传递给该模板的数据;
MarshalHTML() (name string, data any)
}
// Marshal 针对 HTML 内容的解码实现
//
// 参数 v 可以是以下几种可能:
// - string 或是 []byte 将内容作为 HTML 内容直接输出;
// - 实现了 [Marshaler] 接口,则按 [Marshaler.MarshalHTML] 返回的查找模板名称;
// - 其它普通对象,将获取对象的 XMLName 的 struct tag,若不存在则直接采用类型名作为模板名;
// - 其它情况下则是返回 [mimetype.ErrUnsupported];
func Marshal(ctx *web.Context, v any) ([]byte, error) {
if ctx == nil {
panic("参数 ctx 不能为空")
}
switch obj := v.(type) {
case []byte:
return obj, nil
case string:
return []byte(obj), nil
default:
return marshal(ctx, v)
}
}
func marshal(ctx *web.Context, v any) ([]byte, error) {
tt, found := ctx.Server().Vars().Load(viewContextKey)
if !found {
return nil, mimetype.ErrUnsupported()
}
vv := tt.(*view)
tpl := vv.tpl
if vv.dir {
tag, _, _ := vv.b.Matcher().Match(ctx.LanguageTag())
tagName := message.NewPrinter(tag, message.Catalog(vv.b)).Sprintf(tagKey)
t, found := vv.dirTpls[tagName]
if !found { // 理论上不可能出现此种情况,Match 必定会返回一个最相近的语种。
panic(fmt.Sprintf("未找到指定的模板 %s", tagName))
}
tpl = t
}
tpl = tpl.Funcs(template.FuncMap{
"t": func(msg string, v ...any) string { return ctx.Sprintf(msg, v...) },
})
name, v := getName(v)
w := new(bytes.Buffer)
if err := tpl.ExecuteTemplate(w, name, v); err != nil {
return nil, err
}
return w.Bytes(), nil
}
func Unmarshal(io.Reader, any) error { return mimetype.ErrUnsupported() }
func getName(v any) (string, any) {
if m, ok := v.(Marshaler); ok {
return m.MarshalHTML()
}
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
rt := rv.Type()
if rt.Kind() != reflect.Struct {
if name := rt.Name(); name != "" {
return name, v
}
panic(fmt.Sprintf("%s 不支持输出当前类型 %s", Mimetype, rt.Kind()))
}
field, found := rt.FieldByName("XMLName")
if !found {
return rt.Name(), v
}
tag := field.Tag.Get("html")
if tag == "" {
tag = rt.Name()
}
return tag, v
}