-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright (c) 2017, Janoš Guljaš <[email protected]> | ||
// All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package templates | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"html/template" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// NewContextFunc creates a new function that can be used to store | ||
// and access arbitrary data by keys. | ||
func NewContextFunc(m map[string]interface{}) func(string) interface{} { | ||
return func(key string) interface{} { | ||
if value, ok := m[key]; ok { | ||
return value | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
var defaultFunctions = template.FuncMap{ | ||
"safehtml": safeHTMLFunc, | ||
"relative_time": relativeTimeFunc, | ||
"year_range": yearRangeFunc, | ||
"contains_string": containsStringFunc, | ||
"html_br": htmlBrFunc, | ||
"map": mapFunc, | ||
} | ||
|
||
func safeHTMLFunc(text string) template.HTML { | ||
return template.HTML(text) | ||
} | ||
|
||
func relativeTimeFunc(t time.Time) string { | ||
const day = 24 * time.Hour | ||
d := time.Since(t) | ||
switch { | ||
case d < time.Second: | ||
return "just now" | ||
case d < 2*time.Second: | ||
return "one second ago" | ||
case d < time.Minute: | ||
return fmt.Sprintf("%d seconds ago", d/time.Second) | ||
case d < 2*time.Minute: | ||
return "one minute ago" | ||
case d < time.Hour: | ||
return fmt.Sprintf("%d minutes ago", d/time.Minute) | ||
case d < 2*time.Hour: | ||
return "one hour ago" | ||
case d < day: | ||
return fmt.Sprintf("%d hours ago", d/time.Hour) | ||
case d < 2*day: | ||
return "one day ago" | ||
} | ||
return fmt.Sprintf("%d days ago", d/day) | ||
} | ||
|
||
func yearRangeFunc(year int) string { | ||
curYear := time.Now().Year() | ||
if year >= curYear { | ||
return fmt.Sprintf("%d", year) | ||
} | ||
return fmt.Sprintf("%d - %d", year, curYear) | ||
} | ||
|
||
func containsStringFunc(list []string, element, yes, no string) string { | ||
for _, e := range list { | ||
if e == element { | ||
return yes | ||
} | ||
} | ||
return no | ||
} | ||
|
||
func htmlBrFunc(text string) string { | ||
text = template.HTMLEscapeString(text) | ||
return strings.Replace(text, "\n", "<br>", -1) | ||
} | ||
|
||
func mapFunc(values ...interface{}) (map[string]interface{}, error) { | ||
if len(values)%2 != 0 { | ||
return nil, errors.New("invalid map call") | ||
} | ||
m := make(map[string]interface{}, len(values)/2) | ||
for i := 0; i < len(values); i += 2 { | ||
key, ok := values[i].(string) | ||
if !ok { | ||
return nil, errors.New("map keys must be strings") | ||
} | ||
m[key] = values[i+1] | ||
} | ||
return m, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
// Copyright (c) 2017, Janoš Guljaš <[email protected]> | ||
// All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package templates | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"html/template" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"path/filepath" | ||
) | ||
|
||
// Error is a common error type that holds | ||
// information about error message and template name. | ||
type Error struct { | ||
Err error | ||
Template string | ||
} | ||
|
||
func (e *Error) Error() string { | ||
if e.Template == "" { | ||
return e.Err.Error() | ||
} | ||
return fmt.Sprintf("%s: %s", e.Err.Error(), e.Template) | ||
} | ||
|
||
// ErrUnknownTemplate will be returned by Render function if | ||
// the template does not exist. | ||
var ErrUnknownTemplate = fmt.Errorf("unknown template") | ||
|
||
// Options holds parameters for creating Templates. | ||
type Options struct { | ||
baseDir string | ||
contentType string | ||
files map[string][]string | ||
strings map[string][]string | ||
functions template.FuncMap | ||
delimOpen string | ||
delimClose string | ||
logf func(format string, a ...interface{}) | ||
} | ||
|
||
// Option sets parameters used in New function. | ||
type Option func(*Options) | ||
|
||
// WithContentType sets the content type HTTP header that | ||
// will be written on Render and Response functions. | ||
func WithContentType(contentType string) Option { | ||
return func(o *Options) { o.contentType = contentType } | ||
} | ||
|
||
// WithBaseDir sets the directory in which template files | ||
// are stored. | ||
func WithBaseDir(dir string) Option { | ||
return func(o *Options) { o.baseDir = dir } | ||
} | ||
|
||
// WithTemplateFromFiles adds a template parsed from files. | ||
func WithTemplateFromFiles(name string, files ...string) Option { | ||
return func(o *Options) { o.files[name] = files } | ||
} | ||
|
||
// WithTemplatesFromFiles adds a map of templates parsed from files. | ||
func WithTemplatesFromFiles(ts map[string][]string) Option { | ||
return func(o *Options) { | ||
for name, files := range ts { | ||
o.files[name] = files | ||
} | ||
} | ||
} | ||
|
||
// WithTemplateFromStrings adds a template parsed from string. | ||
func WithTemplateFromStrings(name string, strings ...string) Option { | ||
return func(o *Options) { o.strings[name] = strings } | ||
} | ||
|
||
// WithTemplatesFromStrings adds a map of templates parsed from strings. | ||
func WithTemplatesFromStrings(ts map[string][]string) Option { | ||
return func(o *Options) { | ||
for name, strings := range ts { | ||
o.strings[name] = strings | ||
} | ||
} | ||
} | ||
|
||
// WithFunction adds a function to templates. | ||
func WithFunction(name string, fn interface{}) Option { | ||
return func(o *Options) { o.functions[name] = fn } | ||
} | ||
|
||
// WithFunctions adds function map to templates. | ||
func WithFunctions(fns template.FuncMap) Option { | ||
return func(o *Options) { | ||
for name, fn := range fns { | ||
o.functions[name] = fn | ||
} | ||
} | ||
} | ||
|
||
// WithDelims sets the delimiters used in templates. | ||
func WithDelims(open, close string) Option { | ||
return func(o *Options) { | ||
o.delimOpen = open | ||
o.delimClose = close | ||
} | ||
} | ||
|
||
// WithLogFunc sets the function that will perform message logging. | ||
// Default is log.Printf. | ||
func WithLogFunc(logf func(format string, a ...interface{})) Option { | ||
return func(o *Options) { o.logf = logf } | ||
} | ||
|
||
// Templates structure holds parsed templates. | ||
type Templates struct { | ||
templates map[string]*template.Template | ||
defaultName string | ||
contentType string | ||
logf func(format string, a ...interface{}) | ||
} | ||
|
||
// New creates a new instance of Templates and parses | ||
// provided files and strings. | ||
func New(opts ...Option) (t *Templates, err error) { | ||
functions := template.FuncMap{} | ||
for name, fn := range defaultFunctions { | ||
functions[name] = fn | ||
} | ||
o := &Options{ | ||
files: map[string][]string{}, | ||
functions: functions, | ||
delimOpen: "{{", | ||
delimClose: "}}", | ||
logf: log.Printf, | ||
} | ||
for _, opt := range opts { | ||
opt(o) | ||
} | ||
|
||
t = &Templates{ | ||
templates: map[string]*template.Template{}, | ||
contentType: o.contentType, | ||
logf: o.logf, | ||
} | ||
for name, strings := range o.strings { | ||
tpl, err := parseStrings(template.New("").Funcs(o.functions).Delims(o.delimOpen, o.delimClose), strings...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
t.templates[name] = tpl | ||
} | ||
for name, files := range o.files { | ||
fs := []string{} | ||
for _, f := range files { | ||
fs = append(fs, filepath.Join(o.baseDir, f)) | ||
} | ||
tpl, err := parseFiles(template.New("").Funcs(o.functions).Delims(o.delimOpen, o.delimClose), fs...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
t.templates[name] = tpl | ||
} | ||
return | ||
} | ||
|
||
// RespondTemplateWithStatus executes a named template with provided data into buffer, | ||
// then writes the the status and body to the response writer. | ||
// A panic will be raised if the template does not exist or fails to execute. | ||
func (t Templates) RespondTemplateWithStatus(w http.ResponseWriter, name, templateName string, data interface{}, status int) { | ||
buf := bytes.Buffer{} | ||
tpl, ok := t.templates[name] | ||
if !ok { | ||
panic(&Error{Err: ErrUnknownTemplate, Template: name}) | ||
} | ||
if err := tpl.ExecuteTemplate(&buf, templateName, data); err != nil { | ||
panic(err) | ||
} | ||
if t.contentType != "" { | ||
w.Header().Set("Content-Type", t.contentType) | ||
} | ||
if status > 0 { | ||
w.WriteHeader(status) | ||
} | ||
if _, err := buf.WriteTo(w); err != nil { | ||
t.logf("respond %q template %q: %v", name, templateName, err) | ||
} | ||
} | ||
|
||
// RespondWithStatus executes a template with provided data into buffer, | ||
// then writes the the status and body to the response writer. | ||
// A panic will be raised if the template does not exist or fails to execute. | ||
func (t Templates) RespondWithStatus(w http.ResponseWriter, name string, data interface{}, status int) { | ||
buf := bytes.Buffer{} | ||
tpl, ok := t.templates[name] | ||
if !ok { | ||
panic(&Error{Err: ErrUnknownTemplate, Template: name}) | ||
} | ||
if err := tpl.Execute(&buf, data); err != nil { | ||
panic(err) | ||
} | ||
if t.contentType != "" { | ||
w.Header().Set("Content-Type", t.contentType) | ||
} | ||
if status > 0 { | ||
w.WriteHeader(status) | ||
} | ||
if _, err := buf.WriteTo(w); err != nil { | ||
t.logf("respond %q: %v", name, err) | ||
} | ||
} | ||
|
||
// RespondTemplate executes a named template with provided data into buffer, | ||
// then writes the the body to the response writer. | ||
// A panic will be raised if the template does not exist or fails to execute. | ||
func (t Templates) RespondTemplate(w http.ResponseWriter, name, templateName string, data interface{}) { | ||
t.RespondTemplateWithStatus(w, name, templateName, data, 0) | ||
} | ||
|
||
// Respond executes template with provided data into buffer, | ||
// then writes the the body to the response writer. | ||
// A panic will be raised if the template does not exist or fails to execute. | ||
func (t Templates) Respond(w http.ResponseWriter, name string, data interface{}) { | ||
t.RespondWithStatus(w, name, data, 0) | ||
} | ||
|
||
// RenderTemplate executes a named template and returns the string. | ||
func (t Templates) RenderTemplate(name, templateName string, data interface{}) (s string, err error) { | ||
buf := bytes.Buffer{} | ||
tpl, ok := t.templates[name] | ||
if !ok { | ||
return "", &Error{Err: ErrUnknownTemplate, Template: name} | ||
} | ||
if err := tpl.ExecuteTemplate(&buf, templateName, data); err != nil { | ||
return "", err | ||
} | ||
return buf.String(), nil | ||
} | ||
|
||
// Render executes a template and returns the string. | ||
func (t Templates) Render(name string, data interface{}) (s string, err error) { | ||
buf := bytes.Buffer{} | ||
tpl, ok := t.templates[name] | ||
if !ok { | ||
return "", &Error{Err: ErrUnknownTemplate, Template: name} | ||
} | ||
if err := tpl.Execute(&buf, data); err != nil { | ||
return "", err | ||
} | ||
return buf.String(), nil | ||
} | ||
|
||
func parseFiles(t *template.Template, filenames ...string) (*template.Template, error) { | ||
for _, filename := range filenames { | ||
b, err := ioutil.ReadFile(filename) | ||
if err != nil { | ||
return nil, err | ||
} | ||
_, err = t.Parse(string(b)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
return t, nil | ||
} | ||
|
||
func parseStrings(t *template.Template, strings ...string) (*template.Template, error) { | ||
for _, str := range strings { | ||
_, err := t.Parse(str) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
return t, nil | ||
} |