|
|
- // Copyright 2013 Martini Authors
- // Copyright 2014 The Macaron Authors
- //
- // 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"
- "io/ioutil"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "strings"
- "sync"
- "time"
-
- "github.com/Unknwon/com"
- )
-
- const (
- _CONTENT_TYPE = "Content-Type"
- _CONTENT_LENGTH = "Content-Length"
- _CONTENT_BINARY = "application/octet-stream"
- _CONTENT_JSON = "application/json"
- _CONTENT_HTML = "text/html"
- _CONTENT_PLAIN = "text/plain"
- _CONTENT_XHTML = "application/xhtml+xml"
- _CONTENT_XML = "text/xml"
- _DEFAULT_CHARSET = "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
- Get(string) (io.Reader, error)
- }
-
- // 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
- // Addtional directories to overwite templates.
- AppendDirectories []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)
-
- JSON(int, interface{})
- JSONString(interface{}) (string, error)
- RawData(int, []byte) // Serve content as binary
- PlainText(int, []byte) // Serve content as plain text
- 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)
-
- // Directories are composed in reverse order because later one overwrites previous ones,
- // so once found, we can directly jump out of the loop.
- dirs := make([]string, 0, len(opt.AppendDirectories)+1)
- for i := len(opt.AppendDirectories) - 1; i >= 0; i-- {
- dirs = append(dirs, opt.AppendDirectories[i])
- }
- dirs = append(dirs, opt.Directory)
-
- var err error
- for i := range dirs {
- // Skip ones that does not exists for symlink test,
- // but allow non-symlink ones added after start.
- if !com.IsExist(dirs[i]) {
- continue
- }
-
- dirs[i], err = filepath.EvalSymlinks(dirs[i])
- if err != nil {
- panic("EvalSymlinks(" + dirs[i] + "): " + err.Error())
- }
- }
- lastDir := dirs[len(dirs)-1]
-
- // We still walk the last (original) directory because it's non-sense we load templates not exist in original directory.
- if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, err error) error {
- r, err := filepath.Rel(lastDir, path)
- if err != nil {
- return err
- }
-
- ext := GetExt(r)
-
- for _, extension := range opt.Extensions {
- if ext != extension {
- continue
- }
-
- var data []byte
- if !omitData {
- // Loop over candidates of directory, break out once found.
- // The file always exists because it's inside the walk function,
- // and read original file is the worst case.
- for i := range dirs {
- path = filepath.Join(dirs[i], r)
- if !com.IsFile(path) {
- continue
- }
-
- data, err = ioutil.ReadFile(path)
- if err != nil {
- return err
- }
- break
- }
- }
-
- name := filepath.ToSlash((r[0 : len(r)-len(ext)]))
- fs.files = append(fs.files, NewTplFile(name, data, ext))
- }
-
- return nil
- }); err != nil {
- panic("NewTemplateFileSystem: " + err.Error())
- }
-
- return fs
- }
-
- func (fs TplFileSystem) ListFiles() []TemplateFile {
- return fs.files
- }
-
- func (fs TplFileSystem) Get(name string) (io.Reader, error) {
- for i := range fs.files {
- if fs.files[i].Name()+fs.files[i].Ext() == name {
- return bytes.NewReader(fs.files[i].Data()), nil
- }
- }
- return nil, fmt.Errorf("file '%s' not found", name)
- }
-
- func PrepareCharset(charset string) string {
- if len(charset) != 0 {
- return "; charset=" + charset
- }
-
- return "; charset=" + _DEFAULT_CHARSET
- }
-
- func GetExt(s string) string {
- index := strings.Index(s, ".")
- if index == -1 {
- return ""
- }
- return s[index:]
- }
-
- func compile(opt RenderOptions) *template.Template {
- t := template.New(opt.Directory)
- 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
- }
-
- // NewTemplateSet initializes a new empty template set.
- 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 = _CONTENT_HTML
- }
-
- 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) 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(_CONTENT_TYPE, _CONTENT_JSON+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(_CONTENT_TYPE, _CONTENT_XML+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(_CONTENT_TYPE) == "" {
- r.Header().Set(_CONTENT_TYPE, contentType)
- }
- r.WriteHeader(status)
- r.Write(v)
- }
-
- func (r *TplRender) RawData(status int, v []byte) {
- r.data(status, _CONTENT_BINARY, v)
- }
-
- func (r *TplRender) PlainText(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(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset)
- r.WriteHeader(status)
-
- if _, err := out.WriteTo(r); err != nil {
- out.Reset()
- }
- 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
- }
-
- // DummyRender is used when user does not choose any real render to use.
- // This way, we can print out friendly message which asks them to register one,
- // instead of ugly and confusing 'nil pointer' panic.
- type DummyRender struct {
- http.ResponseWriter
- }
-
- func renderNotRegistered() {
- panic("middleware render hasn't been registered")
- }
-
- func (r *DummyRender) SetResponseWriter(http.ResponseWriter) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) JSON(int, interface{}) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) JSONString(interface{}) (string, error) {
- renderNotRegistered()
- return "", nil
- }
-
- func (r *DummyRender) RawData(int, []byte) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) PlainText(int, []byte) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) {
- renderNotRegistered()
- return "", nil
- }
-
- func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) {
- renderNotRegistered()
- return "", nil
- }
-
- func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) {
- renderNotRegistered()
- return nil, nil
- }
-
- func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) {
- renderNotRegistered()
- return nil, nil
- }
-
- func (r *DummyRender) XML(int, interface{}) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) Error(int, ...string) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) Status(int) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) SetTemplatePath(string, string) {
- renderNotRegistered()
- }
-
- func (r *DummyRender) HasTemplateSet(string) bool {
- renderNotRegistered()
- return false
- }
|