123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- // 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,
- }
- }
|