-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathview.go
192 lines (162 loc) · 4.61 KB
/
view.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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package rio
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"strings"
"github.com/tunedmystic/rio/format"
)
// ------------------------------------------------------------------
//
//
// Default View
//
//
// ------------------------------------------------------------------
var defaultView = &View{templates: template.New("")}
// Templates configures the default view with the given
// templateFS filesystem and the
// opts functional options.
func Templates(templatesFS fs.FS, opts ...ViewOpt) {
defaultView = NewView(templatesFS, opts...)
}
// Render writes a template to the http.ResponseWriter.
func Render(w http.ResponseWriter, page string, status int, data any) error {
return defaultView.Render(w, page, status, data)
}
// ------------------------------------------------------------------
//
//
// Functional Options for View
//
//
// ------------------------------------------------------------------
// ViewOpt is a function to configure a View.
type ViewOpt func(*View)
// WithFuncMap adds the functions in the given funcMap to the View.
//
// If the View contains a funcMap function with the same name,
// then it will not be added.
func WithFuncMap(funcMap template.FuncMap) ViewOpt {
return func(v *View) {
for key := range funcMap {
if _, ok := v.funcMap[key]; !ok {
v.funcMap[key] = funcMap[key]
}
}
}
}
// ------------------------------------------------------------------
//
//
// Type: View
//
//
// ------------------------------------------------------------------
// View is a collection of html templates for rendering.
type View struct {
templates *template.Template
funcMap template.FuncMap
}
// NewView constructs and returns a new *View.
//
// The templateFS is a filesystem which contains html templates.
// The opts is a slice of ViewOpt funcs for optional configuration.
func NewView(templatesFS fs.FS, opts ...ViewOpt) *View {
view, err := constructView(templatesFS, opts...)
if err != nil {
panic(fmt.Errorf("failed to construct view: %w", err))
}
return view
}
// Render writes a template to the http.ResponseWriter.
func (v *View) Render(w http.ResponseWriter, page string, status int, data any) error {
buf := getBuffer()
defer putBuffer(buf)
// Write the template to the buffer first.
if err := v.templates.ExecuteTemplate(buf, page, data); err != nil {
return err
}
w.WriteHeader(status)
// Write the contents of the buffer to the http.ResponseWriter.
buf.WriteTo(w)
return nil
}
// constructView constructs and returns a *View.
//
// All html templates within templatesFS are parsed and loaded.
//
// Default template functions are provided to the funcmap, but
// more can be added with the ViewOpt functions.
func constructView(templatesFS fs.FS, opts ...ViewOpt) (*View, error) {
v := &View{
templates: template.New(""),
funcMap: template.FuncMap{},
}
// Set the default template functions.
v.funcMap["safe"] = safeHtml
v.funcMap["title"] = format.Title
v.funcMap["titlefirst"] = format.TitleFirst
// Configure the View with with ViewOpt funcs, if any.
for i := range opts {
opts[i](v)
}
// Parse and load all templates from the given filesystem.
//
// Walk the templateFS filesystem, recursively.
err := fs.WalkDir(templatesFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Process all Html files.
if !d.IsDir() && strings.HasSuffix(path, ".html") {
// Read the file.
fileBytes, err := fs.ReadFile(templatesFS, path)
if err != nil {
return err
}
// Create new template.
t := v.templates.New(path).Funcs(v.funcMap)
// Parse the template.
if _, err := t.Parse(string(fileBytes)); err != nil {
return err
}
}
return nil
})
return v, err
}
// ------------------------------------------------------------------
//
//
// Helpers
//
//
// ------------------------------------------------------------------
// WrapItem wraps a value of any type in a func.
// This is used to inject values into the template.FuncMap.
func WrapItem[T any](val T) func() T {
return func() T {
return val
}
}
// WrapSlice wraps a slice of any type in a func.
// This is used to inject values into the template.FuncMap.
func WrapSlice[T any](val []T) func() []T {
return func() []T {
return val
}
}
// WrapMap wraps a map of type [string]any in a func.
// This is used to inject values into the template.FuncMap.
func WrapMap[T any](val map[string]T) func() map[string]T {
return func() map[string]T {
return val
}
}
// safeHtml converts a string into an HTML fragment, so that
// it can be rendered verbatim in the template.
func safeHtml(content string) template.HTML {
return template.HTML(content)
}