Skip to content

Commit

Permalink
Add templates package
Browse files Browse the repository at this point in the history
  • Loading branch information
janos committed Aug 3, 2017
1 parent f1611f3 commit 01472e4
Show file tree
Hide file tree
Showing 2 changed files with 378 additions and 0 deletions.
99 changes: 99 additions & 0 deletions templates/functions.go
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
}
279 changes: 279 additions & 0 deletions templates/templates.go
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
}

0 comments on commit 01472e4

Please sign in to comment.