-
Notifications
You must be signed in to change notification settings - Fork 0
/
templates.go
134 lines (116 loc) · 3.23 KB
/
templates.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
package oidcauth
import (
"bytes"
"embed"
"fmt"
"html/template"
"io/fs"
"log/slog"
"net/http"
"os"
"sync"
"time"
)
//go:embed templates/*
var templateFS embed.FS
type templateManager struct {
dirs []fs.FS
templates map[string]*template.Template
devMode bool
mu *sync.Mutex
}
func NewTemplateManager(directory string, devMode bool) (*templateManager, error) {
var templates map[string]*template.Template
builtInTemplates, err := fs.Sub(templateFS, "templates")
if err != nil {
return nil, err
}
dirs := []fs.FS{builtInTemplates}
if directory != "" {
dirs = append(dirs, os.DirFS(directory))
}
templates, err = parsePageTemplates(dirs...)
if err != nil {
return nil, err
}
return &templateManager{
dirs: dirs,
templates: templates,
devMode: devMode,
mu: &sync.Mutex{},
}, nil
}
func (t *templateManager) servePage(w http.ResponseWriter, templateName string, data any) {
buf, err := t.renderPage(templateName, data)
if err != nil {
slog.Error("faild to serve page", "template_name", templateName, "err", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "text/html")
_, _ = w.Write(buf)
}
// renderPage executes the given templateName with page.
func (t *templateManager) renderPage(templateName string, data any) ([]byte, error) {
tmpl, err := t.getTemplate(templateName)
if err != nil {
return nil, err
}
return executeTemplate(templateName, tmpl, data)
}
func (t *templateManager) getTemplate(templateName string) (*template.Template, error) {
if t.devMode {
t.mu.Lock()
defer t.mu.Unlock()
var err error
t.templates, err = parsePageTemplates(t.dirs...)
if err != nil {
return nil, fmt.Errorf("error parsing templates: %v", err)
}
}
tmpl := t.templates[templateName]
if tmpl == nil {
return nil, fmt.Errorf("BUG: a.templates[%q] not found", templateName)
}
return tmpl, nil
}
func executeTemplate(templateName string, tmpl *template.Template, data any) ([]byte, error) {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
slog.Error("error executing template", "name", templateName, "err", err)
return nil, err
}
return buf.Bytes(), nil
}
// templates and parsed together with the files in each base directory.
func parsePageTemplates(dirs ...fs.FS) (map[string]*template.Template, error) {
templates := make(map[string]*template.Template)
funcs := map[string]any{
"timeFmt": func(t time.Time) string {
return t.Format(time.RFC3339)
},
}
// TODO: make shared helpers from buitIn available in subsequent dirs
for _, dir := range dirs {
matches, err := fs.Glob(dir, "*.tmpl")
if err != nil {
return nil, err
}
for _, match := range matches {
t, err := template.New(match).Funcs(funcs).ParseFS(dir, match)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %v", match, err)
}
helperGlob := "shared/*.tmpl"
matches, _ := fs.Glob(dir, helperGlob)
if len(matches) > 0 {
if _, err := t.ParseFS(dir, helperGlob); err != nil {
return nil, fmt.Errorf("ParseFS(%q): %v", helperGlob, err)
}
}
templateName := match[:len(match)-len(".tmpl")]
templates[templateName] = t
}
}
return templates, nil
}