~samwhited/tmpl

ref: v0.0.3 tmpl/tmpl.go -rw-r--r-- 6.5 KiB
564939b2Sam Whited Return errors from renderer 1 year, 5 months ago
                                                                                
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// Package tmpl handles loading and rendering HTML templates.
package tmpl // import "code.soquee.net/tmpl"

import (
	"html/template"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"
	"time"

	"golang.org/x/net/xsrftoken"
	"golang.org/x/text/language"
	"golang.org/x/text/message"
	"golang.org/x/text/message/catalog"
	"golang.org/x/tools/godoc/vfs"
)

// Template wraps an "html/template".Template and adds internationalization and
// live reloading functionality for easy development.
type Template struct {
	*template.Template

	devMode bool
	vfs     vfs.FileSystem
	catalog catalog.Catalog
	funcMap template.FuncMap
}

// New creates a new set of HTML templates from *.tmpl files found in
// the root of a virtual filesystem.
// If dev mode is on, templates are live-reloaded instead of being
// pre-generated.
func New(dev bool, fs vfs.FileSystem, c catalog.Catalog, funcMap template.FuncMap) (Template, error) {
	tmpl := template.New("www").Funcs(funcMap)

	t := Template{
		Template: tmpl,
		devMode:  dev,
		vfs:      fs,
		catalog:  c,
		funcMap:  funcMap,
	}

	tmpls, err := fs.ReadDir("/")
	if err != nil {
		return t, err
	}
	for _, finfo := range tmpls {
		name := finfo.Name()
		// Skip files that aren't templates.
		if path.Ext(name) != ".tmpl" {
			continue
		}
		contents, err := vfs.ReadFile(fs, path.Join("/", name))
		if err != nil {
			return t, err
		}

		name = name[:len(name)-5]
		tmpl = template.Must(tmpl.New(name).Parse(string(contents)))
	}
	return t, nil
}

// Execute executes the template, reloading it first if we're in dev mode.
// Since the base template is always a fragment, Execute will reload templates
// in dev mode, and then always return an error.
func (t Template) Execute(wr io.Writer, data interface{}) error {
	if t.devMode {
		tmpl, err := New(t.devMode, t.vfs, t.catalog, t.funcMap)
		if err != nil {
			return err
		}
		t.Template = tmpl.Template
	}
	return t.Template.Execute(wr, data)
}

// ExecuteTemplate executes the named template, reloading it if we're in dev
// mode.
func (t Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
	if t.devMode {
		tmpl, err := New(t.devMode, t.vfs, t.catalog, t.funcMap)
		if err != nil {
			return err
		}
		t.Template = tmpl.Template
	}
	return t.Template.ExecuteTemplate(wr, name, data)
}

// FlashType is the type of a flash message.
type FlashType string

// A list of flash types.
const (
	FlashDanger  FlashType = "danger"
	FlashSuccess FlashType = "success"
	FlashWarn    FlashType = "warn"
)

const (
	flashCookie = "flash"
)

// SetFlash sets a flash message using a cookie.
// Flash messages can also be set when rendering the response, but since this
// will not work for redirects or methods without a response, sometimes we need
// to set a cookie and read the value from there.
func SetFlash(w http.ResponseWriter, flash Flash) {
	http.SetCookie(w, &http.Cookie{
		Name:   flashCookie,
		Value:  string(flash.Type) + ":" + flash.Message,
		Path:   "/",
		Secure: true,
	})
}

// Flash is a flash message which may be used to convey information to the user.
type Flash struct {
	Message string
	Type    FlashType
}

// Page represents data that can apply generally to any page.
type Page struct {
	Title   string
	Path    string
	URL     *url.URL
	Domain  string
	Host    string
	XSRF    string
	Lang    language.Tag
	Printer *message.Printer
	Flash   Flash
	UID     int

	// Data may be set by a template renderer when the template is executed
	// and should not be set by callers of this package (except by setting the
	// extraData parameters on a template renderer).
	// It will contain data that can only be known at render time and not when the
	// renderer is constructed (which may or may not be the same).
	Data interface{}
}

// T attempts to translate the string "s" using p.Printer.
func (p Page) T(key message.Reference, a ...interface{}) string {
	return p.Printer.Sprintf(key, a...)
}

func defaultData(p Page) interface{} {
	return p
}

// RenderFunc is the type used to render templates into pages.
// For more information see Renderer.
type RenderFunc func(uid int, flash Flash, w http.ResponseWriter, r *http.Request, extraData interface{}) error

// Renderer returns a function that can be used to render pages using the
// provided templates.
//
// The data function is used to construct the data passed to the template (which
// should embed the provided Page).
// If it is nil, the page is used.
// If xsrfKey is provided, an XSRF token is constructed and passed to the page.
// If a flash message is passed to the returned function, it is displayed
// immediately and overrides any flash message set in a cookie (without clearing
// the cookie).
// To set a flash message in a cookie (eg. to persist it across a redirect) see
// SetFlash.
func Renderer(
	domain,
	xsrfKey,
	tmplName,
	title string,
	tmpls Template,
	data func(Page) interface{},
) RenderFunc {
	if data == nil {
		data = defaultData
	}
	matcher := tmpls.catalog.Matcher()

	return func(uid int, flash Flash, w http.ResponseWriter, r *http.Request, extraData interface{}) error {
		var xsrfToken string
		if xsrfKey != "" {
			xsrfToken = xsrftoken.Generate(xsrfKey, strconv.Itoa(uid), r.URL.Path)
		}

		if flash.Message == "" {
			cookie, err := r.Cookie(flashCookie)
			const (
				successPrefix = string(FlashSuccess) + ":"
				dangerPrefix  = string(FlashDanger) + ":"
				warnPrefix    = string(FlashWarn) + ":"
			)
			if err == nil && cookie != nil && cookie.Value != "" {
				switch {
				case strings.HasPrefix(cookie.Value, successPrefix):
					flash.Message = cookie.Value[len(successPrefix):]
					flash.Type = FlashSuccess
				case strings.HasPrefix(cookie.Value, dangerPrefix):
					flash.Message = cookie.Value[len(dangerPrefix):]
					flash.Type = FlashDanger
				case strings.HasPrefix(cookie.Value, warnPrefix):
					flash.Message = cookie.Value[len(warnPrefix):]
					flash.Type = FlashWarn
				}
				// Clear the flash message.
				http.SetCookie(w, &http.Cookie{
					Name:    flashCookie,
					Expires: time.Now().Add(-100 * time.Hour),
					MaxAge:  -1,
					Path:    "/",
					Secure:  true,
				})
			}
		}

		/* #nosec */
		acceptLangs, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
		tag, _, _ := matcher.Match(acceptLangs...)

		return tmpls.ExecuteTemplate(w, tmplName, data(Page{
			Title:   title,
			Path:    r.URL.Path,
			URL:     r.URL,
			Host:    r.Host,
			Domain:  domain,
			XSRF:    xsrfToken,
			Flash:   flash,
			UID:     uid,
			Data:    extraData,
			Lang:    tag,
			Printer: message.NewPrinter(tag, message.Catalog(tmpls.catalog)),
		}))
	}
}