pages - pages.go
1 package pages // import "vimagination.zapto.org/pages"
2
3 import (
4 "errors"
5 "fmt"
6 "html/template"
7 "net/http"
8 "os"
9 "sync"
10 )
11
12 type Pages struct {
13 baseTemplate string
14 isFile bool
15 defaultTemplate *template.Template
16
17 mu sync.RWMutex
18 templates map[string]dataFile
19 hook HookFn
20 }
21
22 func New(baseTemplateFilename string) (*Pages, error) {
23 templateSrc, err := os.ReadFile(baseTemplateFilename)
24 if err != nil {
25 return nil, fmt.Errorf("error loading base template (%q): %w", baseTemplateFilename, err)
26 }
27 p, err := NewString(string(templateSrc))
28 if err != nil {
29 return nil, err
30 }
31 p.baseTemplate = baseTemplateFilename
32 p.isFile = true
33 return p, nil
34 }
35
36 func NewString(baseTemplate string) (*Pages, error) {
37 defaultTemplate, err := template.New("").Parse(string(baseTemplate))
38 if err != nil {
39 return nil, fmt.Errorf("error initialising base template: %w", err)
40 }
41 return &Pages{
42 baseTemplate: baseTemplate,
43 defaultTemplate: defaultTemplate,
44 templates: make(map[string]dataFile),
45 hook: PassthroughHook,
46 }, nil
47 }
48
49 func (p *Pages) StaticFile(static string) error {
50 return p.RegisterFile(Static, static)
51 }
52
53 func (p *Pages) StaticString(static string) error {
54 return p.RegisterString(Static, static)
55 }
56
57 type dataFile struct {
58 *template.Template
59 data string
60 isFile bool
61 }
62
63 func (p *Pages) RegisterFile(name, filename string) error {
64 p.mu.Lock()
65 defer p.mu.Unlock()
66 if _, ok := p.templates[name]; ok {
67 return ErrTemplateExists
68 }
69 templateSrc, err := os.ReadFile(filename)
70 if err != nil {
71 return fmt.Errorf("error loading template (%q): %w", filename, err)
72 }
73 dtc, err := p.defaultTemplate.Clone()
74 if err != nil {
75 return fmt.Errorf("error cloning template (%q): %w", filename, err)
76 }
77 t, err := dtc.Parse(string(templateSrc))
78 if err != nil {
79 return fmt.Errorf("error initialising template (%q): %w", filename, err)
80 }
81 p.templates[name] = dataFile{
82 Template: t,
83 data: filename,
84 isFile: true,
85 }
86 return nil
87 }
88
89 func (p *Pages) RegisterString(name, contents string) error {
90 p.mu.Lock()
91 defer p.mu.Unlock()
92 if _, ok := p.templates[name]; ok {
93 return ErrTemplateExists
94 }
95 dtc, err := p.defaultTemplate.Clone()
96 if err != nil {
97 return fmt.Errorf("error cloning template (%q): %w", name, err)
98 }
99 t, err := dtc.Parse(contents)
100 if err != nil {
101 return fmt.Errorf("error initialising template (%q): %w", name, err)
102 }
103 p.templates[name] = dataFile{
104 Template: t,
105 data: contents,
106 }
107 return nil
108 }
109
110 func (p *Pages) Rebuild() error {
111 p.mu.Lock()
112 defer p.mu.Unlock()
113 var (
114 np *Pages
115 err error
116 )
117 if p.isFile {
118 np, err = New(p.baseTemplate)
119 } else {
120 np = &Pages{
121 defaultTemplate: p.defaultTemplate,
122 templates: make(map[string]dataFile),
123 }
124 }
125 if err != nil {
126 return fmt.Errorf("error reloading templates: %w", err)
127 }
128 for name, data := range p.templates {
129 if data.isFile {
130 if err = np.RegisterFile(name, data.data); err != nil {
131 return fmt.Errorf("error reloading templates: %w", err)
132 }
133 } else {
134 if p.isFile {
135 if err = np.RegisterString(name, data.data); err != nil {
136 return fmt.Errorf("error reloading templates: %w", err)
137 }
138 } else {
139 np.templates[name] = data
140 }
141 }
142 }
143 p.defaultTemplate = np.defaultTemplate
144 p.templates = np.templates
145 return nil
146 }
147
148 func (p *Pages) Write(w http.ResponseWriter, r *http.Request, templateName string, data interface{}) error {
149 p.mu.RLock()
150 tmpl, ok := p.templates[templateName]
151 p.mu.RUnlock()
152 if !ok {
153 return fmt.Errorf("%s: %w", templateName, ErrUnknownTemplate)
154 }
155 if err := tmpl.Execute(w, p.hook(w, r, data)); err != nil {
156 return fmt.Errorf("error writing template: %w", err)
157 }
158 return nil
159 }
160
161 func (p *Pages) Hook(hook HookFn) {
162 p.mu.Lock()
163 p.hook = hook
164 p.mu.Unlock()
165 }
166
167 type HookFn func(http.ResponseWriter, *http.Request, interface{}) interface{}
168
169 var PassthroughHook HookFn = func(_ http.ResponseWriter, _ *http.Request, data interface{}) interface{} {
170 return data
171 }
172
173 // Errors
174 var (
175 ErrTemplateExists = errors.New("template already exists")
176 ErrUnknownTemplate = errors.New("unknown template")
177 )
178