599 lines
15 KiB
Go
599 lines
15 KiB
Go
// Copyright 2013 Martini Authors
|
|
// Copyright 2014 Unknwon
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package macaron
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Unknwon/com"
|
|
)
|
|
|
|
const (
|
|
ContentType = "Content-Type"
|
|
ContentLength = "Content-Length"
|
|
ContentBinary = "application/octet-stream"
|
|
ContentJSON = "application/json"
|
|
ContentHTML = "text/html"
|
|
CONTENT_PLAIN = "text/plain"
|
|
ContentXHTML = "application/xhtml+xml"
|
|
ContentXML = "text/xml"
|
|
defaultCharset = "UTF-8"
|
|
)
|
|
|
|
var (
|
|
// Provides a temporary buffer to execute templates into and catch errors.
|
|
bufpool = sync.Pool{
|
|
New: func() interface{} { return new(bytes.Buffer) },
|
|
}
|
|
|
|
// Included helper functions for use when rendering html
|
|
helperFuncs = template.FuncMap{
|
|
"yield": func() (string, error) {
|
|
return "", fmt.Errorf("yield called with no layout defined")
|
|
},
|
|
"current": func() (string, error) {
|
|
return "", nil
|
|
},
|
|
}
|
|
)
|
|
|
|
type (
|
|
// TemplateFile represents a interface of template file that has name and can be read.
|
|
TemplateFile interface {
|
|
Name() string
|
|
Data() []byte
|
|
Ext() string
|
|
}
|
|
// TemplateFileSystem represents a interface of template file system that able to list all files.
|
|
TemplateFileSystem interface {
|
|
ListFiles() []TemplateFile
|
|
}
|
|
|
|
// Delims represents a set of Left and Right delimiters for HTML template rendering
|
|
Delims struct {
|
|
// Left delimiter, defaults to {{
|
|
Left string
|
|
// Right delimiter, defaults to }}
|
|
Right string
|
|
}
|
|
|
|
// RenderOptions represents a struct for specifying configuration options for the Render middleware.
|
|
RenderOptions struct {
|
|
// Directory to load templates. Default is "templates".
|
|
Directory string
|
|
// Layout template name. Will not render a layout if "". Default is to "".
|
|
Layout string
|
|
// Extensions to parse template files from. Defaults are [".tmpl", ".html"].
|
|
Extensions []string
|
|
// Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is [].
|
|
Funcs []template.FuncMap
|
|
// Delims sets the action delimiters to the specified strings in the Delims struct.
|
|
Delims Delims
|
|
// Appends the given charset to the Content-Type header. Default is "UTF-8".
|
|
Charset string
|
|
// Outputs human readable JSON.
|
|
IndentJSON bool
|
|
// Outputs human readable XML.
|
|
IndentXML bool
|
|
// Prefixes the JSON output with the given bytes.
|
|
PrefixJSON []byte
|
|
// Prefixes the XML output with the given bytes.
|
|
PrefixXML []byte
|
|
// Allows changing of output to XHTML instead of HTML. Default is "text/html"
|
|
HTMLContentType string
|
|
// TemplateFileSystem is the interface for supporting any implmentation of template file system.
|
|
TemplateFileSystem
|
|
}
|
|
|
|
// HTMLOptions is a struct for overriding some rendering Options for specific HTML call
|
|
HTMLOptions struct {
|
|
// Layout template name. Overrides Options.Layout.
|
|
Layout string
|
|
}
|
|
|
|
Render interface {
|
|
http.ResponseWriter
|
|
SetResponseWriter(http.ResponseWriter)
|
|
RW() http.ResponseWriter
|
|
|
|
JSON(int, interface{})
|
|
JSONString(interface{}) (string, error)
|
|
RawData(int, []byte)
|
|
RenderData(int, []byte)
|
|
HTML(int, string, interface{}, ...HTMLOptions)
|
|
HTMLSet(int, string, string, interface{}, ...HTMLOptions)
|
|
HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error)
|
|
HTMLString(string, interface{}, ...HTMLOptions) (string, error)
|
|
HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error)
|
|
HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error)
|
|
XML(int, interface{})
|
|
Error(int, ...string)
|
|
Status(int)
|
|
SetTemplatePath(string, string)
|
|
HasTemplateSet(string) bool
|
|
}
|
|
)
|
|
|
|
// TplFile implements TemplateFile interface.
|
|
type TplFile struct {
|
|
name string
|
|
data []byte
|
|
ext string
|
|
}
|
|
|
|
// NewTplFile cerates new template file with given name and data.
|
|
func NewTplFile(name string, data []byte, ext string) *TplFile {
|
|
return &TplFile{name, data, ext}
|
|
}
|
|
|
|
func (f *TplFile) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *TplFile) Data() []byte {
|
|
return f.data
|
|
}
|
|
|
|
func (f *TplFile) Ext() string {
|
|
return f.ext
|
|
}
|
|
|
|
// TplFileSystem implements TemplateFileSystem interface.
|
|
type TplFileSystem struct {
|
|
files []TemplateFile
|
|
}
|
|
|
|
// NewTemplateFileSystem creates new template file system with given options.
|
|
func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem {
|
|
fs := TplFileSystem{}
|
|
fs.files = make([]TemplateFile, 0, 10)
|
|
|
|
if err := filepath.Walk(opt.Directory, func(path string, info os.FileInfo, err error) error {
|
|
r, err := filepath.Rel(opt.Directory, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ext := GetExt(r)
|
|
|
|
for _, extension := range opt.Extensions {
|
|
if ext == extension {
|
|
var data []byte
|
|
if !omitData {
|
|
data, err = ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
name := filepath.ToSlash((r[0 : len(r)-len(ext)]))
|
|
fs.files = append(fs.files, NewTplFile(name, data, ext))
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
panic("NewTemplateFileSystem: " + err.Error())
|
|
}
|
|
|
|
return fs
|
|
}
|
|
|
|
func (fs TplFileSystem) ListFiles() []TemplateFile {
|
|
return fs.files
|
|
}
|
|
|
|
func PrepareCharset(charset string) string {
|
|
if len(charset) != 0 {
|
|
return "; charset=" + charset
|
|
}
|
|
|
|
return "; charset=" + defaultCharset
|
|
}
|
|
|
|
func GetExt(s string) string {
|
|
index := strings.Index(s, ".")
|
|
if index == -1 {
|
|
return ""
|
|
}
|
|
return s[index:]
|
|
}
|
|
|
|
func compile(opt RenderOptions) *template.Template {
|
|
dir := opt.Directory
|
|
t := template.New(dir)
|
|
t.Delims(opt.Delims.Left, opt.Delims.Right)
|
|
// Parse an initial template in case we don't have any.
|
|
template.Must(t.Parse("Macaron"))
|
|
|
|
if opt.TemplateFileSystem == nil {
|
|
opt.TemplateFileSystem = NewTemplateFileSystem(opt, false)
|
|
}
|
|
|
|
for _, f := range opt.TemplateFileSystem.ListFiles() {
|
|
tmpl := t.New(f.Name())
|
|
for _, funcs := range opt.Funcs {
|
|
tmpl.Funcs(funcs)
|
|
}
|
|
// Bomb out if parse fails. We don't want any silent server starts.
|
|
template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data())))
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
const (
|
|
_DEFAULT_TPL_SET_NAME = "DEFAULT"
|
|
)
|
|
|
|
// templateSet represents a template set of type *template.Template.
|
|
type templateSet struct {
|
|
lock sync.RWMutex
|
|
sets map[string]*template.Template
|
|
dirs map[string]string
|
|
}
|
|
|
|
func newTemplateSet() *templateSet {
|
|
return &templateSet{
|
|
sets: make(map[string]*template.Template),
|
|
dirs: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (ts *templateSet) Set(name string, opt *RenderOptions) *template.Template {
|
|
t := compile(*opt)
|
|
|
|
ts.lock.Lock()
|
|
defer ts.lock.Unlock()
|
|
|
|
ts.sets[name] = t
|
|
ts.dirs[name] = opt.Directory
|
|
return t
|
|
}
|
|
|
|
func (ts *templateSet) Get(name string) *template.Template {
|
|
ts.lock.RLock()
|
|
defer ts.lock.RUnlock()
|
|
|
|
return ts.sets[name]
|
|
}
|
|
|
|
func (ts *templateSet) GetDir(name string) string {
|
|
ts.lock.RLock()
|
|
defer ts.lock.RUnlock()
|
|
|
|
return ts.dirs[name]
|
|
}
|
|
|
|
func prepareRenderOptions(options []RenderOptions) RenderOptions {
|
|
var opt RenderOptions
|
|
if len(options) > 0 {
|
|
opt = options[0]
|
|
}
|
|
|
|
// Defaults.
|
|
if len(opt.Directory) == 0 {
|
|
opt.Directory = "templates"
|
|
}
|
|
if len(opt.Extensions) == 0 {
|
|
opt.Extensions = []string{".tmpl", ".html"}
|
|
}
|
|
if len(opt.HTMLContentType) == 0 {
|
|
opt.HTMLContentType = ContentHTML
|
|
}
|
|
|
|
return opt
|
|
}
|
|
|
|
func ParseTplSet(tplSet string) (tplName string, tplDir string) {
|
|
tplSet = strings.TrimSpace(tplSet)
|
|
if len(tplSet) == 0 {
|
|
panic("empty template set argument")
|
|
}
|
|
infos := strings.Split(tplSet, ":")
|
|
if len(infos) == 1 {
|
|
tplDir = infos[0]
|
|
tplName = path.Base(tplDir)
|
|
} else {
|
|
tplName = infos[0]
|
|
tplDir = infos[1]
|
|
}
|
|
|
|
if !com.IsDir(tplDir) {
|
|
panic("template set path does not exist or is not a directory")
|
|
}
|
|
return tplName, tplDir
|
|
}
|
|
|
|
func renderHandler(opt RenderOptions, tplSets []string) Handler {
|
|
cs := PrepareCharset(opt.Charset)
|
|
ts := newTemplateSet()
|
|
ts.Set(_DEFAULT_TPL_SET_NAME, &opt)
|
|
|
|
var tmpOpt RenderOptions
|
|
for _, tplSet := range tplSets {
|
|
tplName, tplDir := ParseTplSet(tplSet)
|
|
tmpOpt = opt
|
|
tmpOpt.Directory = tplDir
|
|
ts.Set(tplName, &tmpOpt)
|
|
}
|
|
|
|
return func(ctx *Context) {
|
|
r := &TplRender{
|
|
ResponseWriter: ctx.Resp,
|
|
templateSet: ts,
|
|
Opt: &opt,
|
|
CompiledCharset: cs,
|
|
}
|
|
ctx.Data["TmplLoadTimes"] = func() string {
|
|
if r.startTime.IsZero() {
|
|
return ""
|
|
}
|
|
return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms"
|
|
}
|
|
|
|
ctx.Render = r
|
|
ctx.MapTo(r, (*Render)(nil))
|
|
}
|
|
}
|
|
|
|
// Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain.
|
|
// An single variadic macaron.RenderOptions struct can be optionally provided to configure
|
|
// HTML rendering. The default directory for templates is "templates" and the default
|
|
// file extension is ".tmpl" and ".html".
|
|
//
|
|
// If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
|
|
// MACARON_ENV environment variable to "production".
|
|
func Renderer(options ...RenderOptions) Handler {
|
|
return renderHandler(prepareRenderOptions(options), []string{})
|
|
}
|
|
|
|
func Renderers(options RenderOptions, tplSets ...string) Handler {
|
|
return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets)
|
|
}
|
|
|
|
type TplRender struct {
|
|
http.ResponseWriter
|
|
*templateSet
|
|
Opt *RenderOptions
|
|
CompiledCharset string
|
|
|
|
startTime time.Time
|
|
}
|
|
|
|
func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) {
|
|
r.ResponseWriter = rw
|
|
}
|
|
|
|
func (r *TplRender) RW() http.ResponseWriter {
|
|
return r.ResponseWriter
|
|
}
|
|
|
|
func (r *TplRender) JSON(status int, v interface{}) {
|
|
var (
|
|
result []byte
|
|
err error
|
|
)
|
|
if r.Opt.IndentJSON {
|
|
result, err = json.MarshalIndent(v, "", " ")
|
|
} else {
|
|
result, err = json.Marshal(v)
|
|
}
|
|
if err != nil {
|
|
http.Error(r, err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
// json rendered fine, write out the result
|
|
r.Header().Set(ContentType, ContentJSON+r.CompiledCharset)
|
|
r.WriteHeader(status)
|
|
if len(r.Opt.PrefixJSON) > 0 {
|
|
r.Write(r.Opt.PrefixJSON)
|
|
}
|
|
r.Write(result)
|
|
}
|
|
|
|
func (r *TplRender) JSONString(v interface{}) (string, error) {
|
|
var result []byte
|
|
var err error
|
|
if r.Opt.IndentJSON {
|
|
result, err = json.MarshalIndent(v, "", " ")
|
|
} else {
|
|
result, err = json.Marshal(v)
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(result), nil
|
|
}
|
|
|
|
func (r *TplRender) XML(status int, v interface{}) {
|
|
var result []byte
|
|
var err error
|
|
if r.Opt.IndentXML {
|
|
result, err = xml.MarshalIndent(v, "", " ")
|
|
} else {
|
|
result, err = xml.Marshal(v)
|
|
}
|
|
if err != nil {
|
|
http.Error(r, err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
// XML rendered fine, write out the result
|
|
r.Header().Set(ContentType, ContentXML+r.CompiledCharset)
|
|
r.WriteHeader(status)
|
|
if len(r.Opt.PrefixXML) > 0 {
|
|
r.Write(r.Opt.PrefixXML)
|
|
}
|
|
r.Write(result)
|
|
}
|
|
|
|
func (r *TplRender) data(status int, contentType string, v []byte) {
|
|
if r.Header().Get(ContentType) == "" {
|
|
r.Header().Set(ContentType, contentType)
|
|
}
|
|
r.WriteHeader(status)
|
|
r.Write(v)
|
|
}
|
|
|
|
func (r *TplRender) RawData(status int, v []byte) {
|
|
r.data(status, ContentBinary, v)
|
|
}
|
|
|
|
func (r *TplRender) RenderData(status int, v []byte) {
|
|
r.data(status, CONTENT_PLAIN, v)
|
|
}
|
|
|
|
func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) {
|
|
buf := bufpool.Get().(*bytes.Buffer)
|
|
return buf, t.ExecuteTemplate(buf, name, data)
|
|
}
|
|
|
|
func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) {
|
|
funcs := template.FuncMap{
|
|
"yield": func() (template.HTML, error) {
|
|
buf, err := r.execute(t, tplName, data)
|
|
// return safe html here since we are rendering our own template
|
|
return template.HTML(buf.String()), err
|
|
},
|
|
"current": func() (string, error) {
|
|
return tplName, nil
|
|
},
|
|
}
|
|
t.Funcs(funcs)
|
|
}
|
|
|
|
func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) {
|
|
t := r.templateSet.Get(setName)
|
|
if Env == DEV {
|
|
opt := *r.Opt
|
|
opt.Directory = r.templateSet.GetDir(setName)
|
|
t = r.templateSet.Set(setName, &opt)
|
|
}
|
|
if t == nil {
|
|
return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName)
|
|
}
|
|
|
|
opt := r.prepareHTMLOptions(htmlOpt)
|
|
|
|
if len(opt.Layout) > 0 {
|
|
r.addYield(t, tplName, data)
|
|
tplName = opt.Layout
|
|
}
|
|
|
|
out, err := r.execute(t, tplName, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
|
|
r.startTime = time.Now()
|
|
|
|
out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
|
|
if err != nil {
|
|
http.Error(r, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
r.Header().Set(ContentType, r.Opt.HTMLContentType+r.CompiledCharset)
|
|
r.WriteHeader(status)
|
|
|
|
out.WriteTo(r)
|
|
bufpool.Put(out)
|
|
}
|
|
|
|
func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) {
|
|
r.renderHTML(status, _DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
|
|
}
|
|
|
|
func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
|
|
r.renderHTML(status, setName, tplName, data, htmlOpt...)
|
|
}
|
|
|
|
func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
|
|
out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
|
|
if err != nil {
|
|
return []byte(""), err
|
|
}
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
|
|
return r.HTMLSetBytes(_DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
|
|
}
|
|
|
|
func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
|
|
p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...)
|
|
return string(p), err
|
|
}
|
|
|
|
func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
|
|
p, err := r.HTMLBytes(name, data, htmlOpt...)
|
|
return string(p), err
|
|
}
|
|
|
|
// Error writes the given HTTP status to the current ResponseWriter
|
|
func (r *TplRender) Error(status int, message ...string) {
|
|
r.WriteHeader(status)
|
|
if len(message) > 0 {
|
|
r.Write([]byte(message[0]))
|
|
}
|
|
}
|
|
|
|
func (r *TplRender) Status(status int) {
|
|
r.WriteHeader(status)
|
|
}
|
|
|
|
func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
|
|
if len(htmlOpt) > 0 {
|
|
return htmlOpt[0]
|
|
}
|
|
|
|
return HTMLOptions{
|
|
Layout: r.Opt.Layout,
|
|
}
|
|
}
|
|
|
|
func (r *TplRender) SetTemplatePath(setName, dir string) {
|
|
if len(setName) == 0 {
|
|
setName = _DEFAULT_TPL_SET_NAME
|
|
}
|
|
opt := *r.Opt
|
|
opt.Directory = dir
|
|
r.templateSet.Set(setName, &opt)
|
|
}
|
|
|
|
func (r *TplRender) HasTemplateSet(name string) bool {
|
|
return r.templateSet.Get(name) != nil
|
|
}
|