387 lines
10 KiB
Go
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,
|
||
|
}
|
||
|
}
|