go-skeletor/vendor/github.com/martini-contrib/render/render.go
2021-11-02 11:25:21 +01:00

387 lines
10 KiB
Go

// Package render is a middleware for Martini that provides easy JSON serialization and HTML template rendering.
//
// package main
//
// import (
// "encoding/xml"
//
// "github.com/go-martini/martini"
// "github.com/martini-contrib/render"
// )
//
// type Greeting struct {
// XMLName xml.Name `xml:"greeting"`
// One string `xml:"one,attr"`
// Two string `xml:"two,attr"`
// }
//
// func main() {
// m := martini.Classic()
// m.Use(render.Renderer()) // reads "templates" directory by default
//
// m.Get("/html", func(r render.Render) {
// r.HTML(200, "mytemplate", nil)
// })
//
// m.Get("/json", func(r render.Render) {
// r.JSON(200, "hello world")
// })
//
// m.Get("/xml", func(r render.Render) {
// r.XML(200, Greeting{One: "hello", Two: "world"})
// })
//
// m.Run()
// }
package render
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"html/template"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/oxtoacart/bpool"
"github.com/go-martini/martini"
)
const (
ContentType = "Content-Type"
ContentLength = "Content-Length"
ContentBinary = "application/octet-stream"
ContentText = "text/plain"
ContentJSON = "application/json"
ContentHTML = "text/html"
ContentXHTML = "application/xhtml+xml"
ContentXML = "text/xml"
defaultCharset = "UTF-8"
)
// Provides a temporary buffer to execute templates into and catch errors.
var bufpool *bpool.BufferPool
// Included helper functions for use when rendering html
var helperFuncs = template.FuncMap{
"yield": func() (string, error) {
return "", fmt.Errorf("yield called with no layout defined")
},
"current": func() (string, error) {
return "", nil
},
}
// Render is a service that can be injected into a Martini handler. Render provides functions for easily writing JSON and
// HTML templates out to a http Response.
type Render interface {
// JSON writes the given status and JSON serialized version of the given value to the http.ResponseWriter.
JSON(status int, v interface{})
// HTML renders a html template specified by the name and writes the result and given status to the http.ResponseWriter.
HTML(status int, name string, v interface{}, htmlOpt ...HTMLOptions)
// XML writes the given status and XML serialized version of the given value to the http.ResponseWriter.
XML(status int, v interface{})
// Data writes the raw byte array to the http.ResponseWriter.
Data(status int, v []byte)
// Text writes the given status and plain text to the http.ResponseWriter.
Text(status int, v string)
// Error is a convenience function that writes an http status to the http.ResponseWriter.
Error(status int)
// Status is an alias for Error (writes an http status to the http.ResponseWriter)
Status(status int)
// Redirect is a convienience function that sends an HTTP redirect. If status is omitted, uses 302 (Found)
Redirect(location string, status ...int)
// Template returns the internal *template.Template used to render the HTML
Template() *template.Template
// Header exposes the header struct from http.ResponseWriter.
Header() http.Header
}
// Delims represents a set of Left and Right delimiters for HTML template rendering
type Delims struct {
// Left delimiter, defaults to {{
Left string
// Right delimiter, defaults to }}
Right string
}
// Options is a struct for specifying configuration options for the render.Renderer middleware
type Options struct {
// Directory to load templates. Default is "templates"
Directory string
// Layout template name. Will not render a layout if "". Defaults to "".
Layout string
// Extensions to parse template files from. Defaults to [".tmpl"]
Extensions []string
// Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Defaults to [].
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
}
// HTMLOptions is a struct for overriding some rendering Options for specific HTML call
type HTMLOptions struct {
// Layout template name. Overrides Options.Layout.
Layout string
}
// Renderer is a Middleware that maps a render.Render service into the Martini handler chain. An single variadic render.Options
// struct can be optionally provided to configure HTML rendering. The default directory for templates is "templates" and the default
// file extension is ".tmpl".
//
// If MARTINI_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
// MARTINI_ENV environment variable to "production"
func Renderer(options ...Options) martini.Handler {
opt := prepareOptions(options)
cs := prepareCharset(opt.Charset)
t := compile(opt)
bufpool = bpool.NewBufferPool(64)
return func(res http.ResponseWriter, req *http.Request, c martini.Context) {
var tc *template.Template
if martini.Env == martini.Dev {
// recompile for easy development
tc = compile(opt)
} else {
// use a clone of the initial template
tc, _ = t.Clone()
}
c.MapTo(&renderer{res, req, tc, opt, cs}, (*Render)(nil))
}
}
func prepareCharset(charset string) string {
if len(charset) != 0 {
return "; charset=" + charset
}
return "; charset=" + defaultCharset
}
func prepareOptions(options []Options) Options {
var opt Options
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"}
}
if len(opt.HTMLContentType) == 0 {
opt.HTMLContentType = ContentHTML
}
return opt
}
func compile(options Options) *template.Template {
dir := options.Directory
t := template.New(dir)
t.Delims(options.Delims.Left, options.Delims.Right)
// parse an initial template in case we don't have any
template.Must(t.Parse("Martini"))
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
r, err := filepath.Rel(dir, path)
if err != nil {
return err
}
ext := getExt(r)
for _, extension := range options.Extensions {
if ext == extension {
buf, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
name := (r[0 : len(r)-len(ext)])
tmpl := t.New(filepath.ToSlash(name))
// add our funcmaps
for _, funcs := range options.Funcs {
tmpl.Funcs(funcs)
}
// Bomb out if parse fails. We don't want any silent server starts.
template.Must(tmpl.Funcs(helperFuncs).Parse(string(buf)))
break
}
}
return nil
})
return t
}
func getExt(s string) string {
if strings.Index(s, ".") == -1 {
return ""
}
return "." + strings.Join(strings.Split(s, ".")[1:], ".")
}
type renderer struct {
http.ResponseWriter
req *http.Request
t *template.Template
opt Options
compiledCharset string
}
func (r *renderer) JSON(status int, v interface{}) {
var result []byte
var 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 *renderer) HTML(status int, name string, binding interface{}, htmlOpt ...HTMLOptions) {
opt := r.prepareHTMLOptions(htmlOpt)
// assign a layout if there is one
if len(opt.Layout) > 0 {
r.addYield(name, binding)
name = opt.Layout
}
buf, err := r.execute(name, binding)
if err != nil {
http.Error(r, err.Error(), http.StatusInternalServerError)
return
}
// template rendered fine, write out the result
r.Header().Set(ContentType, r.opt.HTMLContentType+r.compiledCharset)
r.WriteHeader(status)
io.Copy(r, buf)
bufpool.Put(buf)
}
func (r *renderer) 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 *renderer) Data(status int, v []byte) {
if r.Header().Get(ContentType) == "" {
r.Header().Set(ContentType, ContentBinary)
}
r.WriteHeader(status)
r.Write(v)
}
func (r *renderer) Text(status int, v string) {
if r.Header().Get(ContentType) == "" {
r.Header().Set(ContentType, ContentText+r.compiledCharset)
}
r.WriteHeader(status)
r.Write([]byte(v))
}
// Error writes the given HTTP status to the current ResponseWriter
func (r *renderer) Error(status int) {
r.WriteHeader(status)
}
func (r *renderer) Status(status int) {
r.WriteHeader(status)
}
func (r *renderer) Redirect(location string, status ...int) {
code := http.StatusFound
if len(status) == 1 {
code = status[0]
}
http.Redirect(r, r.req, location, code)
}
func (r *renderer) Template() *template.Template {
return r.t
}
func (r *renderer) execute(name string, binding interface{}) (*bytes.Buffer, error) {
buf := bufpool.Get()
return buf, r.t.ExecuteTemplate(buf, name, binding)
}
func (r *renderer) addYield(name string, binding interface{}) {
funcs := template.FuncMap{
"yield": func() (template.HTML, error) {
buf, err := r.execute(name, binding)
// return safe html here since we are rendering our own template
return template.HTML(buf.String()), err
},
"current": func() (string, error) {
return name, nil
},
}
r.t.Funcs(funcs)
}
func (r *renderer) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
if len(htmlOpt) > 0 {
return htmlOpt[0]
}
return HTMLOptions{
Layout: r.opt.Layout,
}
}