kuvia2/vendor/github.com/flamego/template/template.go
2022-01-15 00:09:03 +01:00

246 lines
6.3 KiB
Go

// Copyright 2021 Flamego. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package template
import (
"bytes"
"fmt"
gotemplate "html/template"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"github.com/pkg/errors"
"github.com/flamego/flamego"
)
// Template is a Go template rendering engine.
type Template interface {
// HTML renders the named template with the given status.
HTML(status int, name string)
}
var _ Template = (*template)(nil)
type template struct {
responseWriter flamego.ResponseWriter
logger *log.Logger
*gotemplate.Template
Data
contentType string
bufPool *sync.Pool
}
func responseServerError(w http.ResponseWriter, err error) {
if flamego.Env() == flamego.EnvTypeDev {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
func (t *template) HTML(status int, name string) {
buf := t.bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
t.bufPool.Put(buf)
}()
started := time.Now()
t.Data["RenderDuration"] = func() string {
return fmt.Sprint(time.Since(started).Nanoseconds()/1e6) + "ms"
}
err := t.ExecuteTemplate(buf, name, t.Data)
if err != nil {
responseServerError(t.responseWriter, err)
return
}
t.responseWriter.Header().Set("Content-Type", t.contentType+"; charset=utf-8")
t.responseWriter.WriteHeader(status)
_, err = buf.WriteTo(t.responseWriter)
if err != nil {
t.logger.Printf("template: failed to write out rendered HTML: %v", err)
return
}
}
// Data is used as the root object for rendering a template.
type Data map[string]interface{}
// Delims is a pair of Left and Right delimiters for rendering HTML templates.
type Delims struct {
// Left is the left delimiter. Default is "{{".
Left string
// Right is the right delimiter. Default is "}}".
Right string
}
// Options contains options for the template.Templater middleware.
type Options struct {
// FileSystem is the interface for supporting any implementation of the
// FileSystem.
FileSystem FileSystem
// Directory is the primary directory to load templates. This value is ignored
// when FileSystem is set. Default is "templates".
Directory string
// AppendDirectories is a list of additional directories to load templates for
// overwriting templates that are loaded from FileSystem or Directory.
AppendDirectories []string
// Extensions is a list of extensions to be used for template files. Default is
// `[".tmpl", ".html"]`.
Extensions []string
// FuncMaps is a list of `template.FuncMap` to be applied for rendering
// templates.
FuncMaps []gotemplate.FuncMap
// Delims is the pair of left and right delimiters for rendering templates.
Delims Delims
// ContentType specifies the value of "Content-Type". Default is "text/html".
ContentType string
}
func newTemplate(allowedExtensions []string, funcMaps []gotemplate.FuncMap, delmis Delims, fs FileSystem, dir string, others ...string) (*gotemplate.Template, error) {
if fs == nil {
var err error
fs, err = newFileSystem(dir, allowedExtensions)
if err != nil {
return nil, errors.Wrapf(err, "new file system")
}
}
// Directories are composed in the reverse order because later ones overwrites
// previous ones. Therefore, we can simply break of the loop once found an
// overwritten when looping in the reverse order.
dirs := make([]string, 0, len(others))
for i := len(others) - 1; i >= 0; i-- {
dirs = append(dirs, others[i])
}
for i := range dirs {
if !isDir(dirs[i]) {
continue
}
var err error
dirs[i], err = filepath.EvalSymlinks(dirs[i])
if err != nil {
return nil, errors.Wrapf(err, "eval symlinks for %q", dirs[i])
}
}
tpl := gotemplate.New("Flamego.Template").Delims(delmis.Left, delmis.Right)
for _, f := range fs.Files() {
t := tpl.New(f.Name())
for _, funcMap := range funcMaps {
t.Funcs(funcMap)
}
var err error
var data []byte
// Loop over append directories and break out once found.
for _, dir := range dirs {
fpath := filepath.Join(dir, f.Name()+f.Ext())
if !isFile(fpath) {
continue
}
data, err = os.ReadFile(fpath)
if err != nil {
return nil, errors.Wrap(err, "read")
}
break
}
if len(data) == 0 {
data, err = f.Data()
if err != nil {
return nil, errors.Wrapf(err, "get data of %q", f.Name())
}
}
_, err = t.Parse(string(data))
if err != nil {
return nil, errors.Wrapf(err, "parse %q", f.Name())
}
}
return tpl, nil
}
// Templater returns a middleware handler that injects template.Templater and
// template.Data into the request context, which are used for rendering
// templates to the ResponseWriter.
//
// When running with flamego.EnvTypeDev, if either Directory or
// AppendDirectories is specified, templates will be recompiled upon every
// request.
func Templater(opts ...Options) flamego.Handler {
var opt Options
if len(opts) > 0 {
opt = opts[0]
}
parseOptions := func(opts Options) Options {
if opts.Directory == "" {
opts.Directory = "templates"
}
if len(opts.Extensions) == 0 {
opts.Extensions = []string{".tmpl", ".html"}
}
if opts.ContentType == "" {
opts.ContentType = "text/html"
}
return opts
}
opt = parseOptions(opt)
tpl, err := newTemplate(opt.Extensions, opt.FuncMaps, opt.Delims, opt.FileSystem, opt.Directory, opt.AppendDirectories...)
if err != nil {
panic("template: new template: " + err.Error())
}
bufPool := &sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
return flamego.LoggerInvoker(func(c flamego.Context, log *log.Logger) {
t := &template{
responseWriter: c.ResponseWriter(),
logger: log,
Template: tpl,
Data: make(Data),
contentType: opt.ContentType,
bufPool: bufPool,
}
if flamego.Env() == flamego.EnvTypeDev &&
(opt.Directory != "" || len(opt.AppendDirectories) > 0) {
tpl, err := newTemplate(opt.Extensions, opt.FuncMaps, opt.Delims, opt.FileSystem, opt.Directory, opt.AppendDirectories...)
if err != nil {
http.Error(
c.ResponseWriter(),
fmt.Sprintf("template: %v", err),
http.StatusInternalServerError,
)
return
}
t.Template = tpl
}
c.MapTo(t, (*Template)(nil))
c.Map(t.Data)
})
}