render.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. // Package render is a middleware for Martini that provides easy JSON serialization and HTML template rendering.
  2. //
  3. // package main
  4. //
  5. // import (
  6. // "encoding/xml"
  7. //
  8. // "github.com/go-martini/martini"
  9. // "github.com/martini-contrib/render"
  10. // )
  11. //
  12. // type Greeting struct {
  13. // XMLName xml.Name `xml:"greeting"`
  14. // One string `xml:"one,attr"`
  15. // Two string `xml:"two,attr"`
  16. // }
  17. //
  18. // func main() {
  19. // m := martini.Classic()
  20. // m.Use(render.Renderer()) // reads "templates" directory by default
  21. //
  22. // m.Get("/html", func(r render.Render) {
  23. // r.HTML(200, "mytemplate", nil)
  24. // })
  25. //
  26. // m.Get("/json", func(r render.Render) {
  27. // r.JSON(200, "hello world")
  28. // })
  29. //
  30. // m.Get("/xml", func(r render.Render) {
  31. // r.XML(200, Greeting{One: "hello", Two: "world"})
  32. // })
  33. //
  34. // m.Run()
  35. // }
  36. package render
  37. import (
  38. "bytes"
  39. "encoding/json"
  40. "encoding/xml"
  41. "fmt"
  42. "html/template"
  43. "io"
  44. "io/ioutil"
  45. "net/http"
  46. "os"
  47. "path/filepath"
  48. "strings"
  49. "github.com/oxtoacart/bpool"
  50. "github.com/go-martini/martini"
  51. )
  52. const (
  53. ContentType = "Content-Type"
  54. ContentLength = "Content-Length"
  55. ContentBinary = "application/octet-stream"
  56. ContentText = "text/plain"
  57. ContentJSON = "application/json"
  58. ContentHTML = "text/html"
  59. ContentXHTML = "application/xhtml+xml"
  60. ContentXML = "text/xml"
  61. defaultCharset = "UTF-8"
  62. )
  63. // Provides a temporary buffer to execute templates into and catch errors.
  64. var bufpool *bpool.BufferPool
  65. // Included helper functions for use when rendering html
  66. var helperFuncs = template.FuncMap{
  67. "yield": func() (string, error) {
  68. return "", fmt.Errorf("yield called with no layout defined")
  69. },
  70. "current": func() (string, error) {
  71. return "", nil
  72. },
  73. }
  74. // Render is a service that can be injected into a Martini handler. Render provides functions for easily writing JSON and
  75. // HTML templates out to a http Response.
  76. type Render interface {
  77. // JSON writes the given status and JSON serialized version of the given value to the http.ResponseWriter.
  78. JSON(status int, v interface{})
  79. // HTML renders a html template specified by the name and writes the result and given status to the http.ResponseWriter.
  80. HTML(status int, name string, v interface{}, htmlOpt ...HTMLOptions)
  81. // XML writes the given status and XML serialized version of the given value to the http.ResponseWriter.
  82. XML(status int, v interface{})
  83. // Data writes the raw byte array to the http.ResponseWriter.
  84. Data(status int, v []byte)
  85. // Text writes the given status and plain text to the http.ResponseWriter.
  86. Text(status int, v string)
  87. // Error is a convenience function that writes an http status to the http.ResponseWriter.
  88. Error(status int)
  89. // Status is an alias for Error (writes an http status to the http.ResponseWriter)
  90. Status(status int)
  91. // Redirect is a convienience function that sends an HTTP redirect. If status is omitted, uses 302 (Found)
  92. Redirect(location string, status ...int)
  93. // Template returns the internal *template.Template used to render the HTML
  94. Template() *template.Template
  95. // Header exposes the header struct from http.ResponseWriter.
  96. Header() http.Header
  97. }
  98. // Delims represents a set of Left and Right delimiters for HTML template rendering
  99. type Delims struct {
  100. // Left delimiter, defaults to {{
  101. Left string
  102. // Right delimiter, defaults to }}
  103. Right string
  104. }
  105. // Options is a struct for specifying configuration options for the render.Renderer middleware
  106. type Options struct {
  107. // Directory to load templates. Default is "templates"
  108. Directory string
  109. // Layout template name. Will not render a layout if "". Defaults to "".
  110. Layout string
  111. // Extensions to parse template files from. Defaults to [".tmpl"]
  112. Extensions []string
  113. // Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Defaults to [].
  114. Funcs []template.FuncMap
  115. // Delims sets the action delimiters to the specified strings in the Delims struct.
  116. Delims Delims
  117. // Appends the given charset to the Content-Type header. Default is "UTF-8".
  118. Charset string
  119. // Outputs human readable JSON
  120. IndentJSON bool
  121. // Outputs human readable XML
  122. IndentXML bool
  123. // Prefixes the JSON output with the given bytes.
  124. PrefixJSON []byte
  125. // Prefixes the XML output with the given bytes.
  126. PrefixXML []byte
  127. // Allows changing of output to XHTML instead of HTML. Default is "text/html"
  128. HTMLContentType string
  129. }
  130. // HTMLOptions is a struct for overriding some rendering Options for specific HTML call
  131. type HTMLOptions struct {
  132. // Layout template name. Overrides Options.Layout.
  133. Layout string
  134. }
  135. // Renderer is a Middleware that maps a render.Render service into the Martini handler chain. An single variadic render.Options
  136. // struct can be optionally provided to configure HTML rendering. The default directory for templates is "templates" and the default
  137. // file extension is ".tmpl".
  138. //
  139. // If MARTINI_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
  140. // MARTINI_ENV environment variable to "production"
  141. func Renderer(options ...Options) martini.Handler {
  142. opt := prepareOptions(options)
  143. cs := prepareCharset(opt.Charset)
  144. t := compile(opt)
  145. bufpool = bpool.NewBufferPool(64)
  146. return func(res http.ResponseWriter, req *http.Request, c martini.Context) {
  147. var tc *template.Template
  148. if martini.Env == martini.Dev {
  149. // recompile for easy development
  150. tc = compile(opt)
  151. } else {
  152. // use a clone of the initial template
  153. tc, _ = t.Clone()
  154. }
  155. c.MapTo(&renderer{res, req, tc, opt, cs}, (*Render)(nil))
  156. }
  157. }
  158. func prepareCharset(charset string) string {
  159. if len(charset) != 0 {
  160. return "; charset=" + charset
  161. }
  162. return "; charset=" + defaultCharset
  163. }
  164. func prepareOptions(options []Options) Options {
  165. var opt Options
  166. if len(options) > 0 {
  167. opt = options[0]
  168. }
  169. // Defaults
  170. if len(opt.Directory) == 0 {
  171. opt.Directory = "templates"
  172. }
  173. if len(opt.Extensions) == 0 {
  174. opt.Extensions = []string{".tmpl"}
  175. }
  176. if len(opt.HTMLContentType) == 0 {
  177. opt.HTMLContentType = ContentHTML
  178. }
  179. return opt
  180. }
  181. func compile(options Options) *template.Template {
  182. dir := options.Directory
  183. t := template.New(dir)
  184. t.Delims(options.Delims.Left, options.Delims.Right)
  185. // parse an initial template in case we don't have any
  186. template.Must(t.Parse("Martini"))
  187. filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
  188. r, err := filepath.Rel(dir, path)
  189. if err != nil {
  190. return err
  191. }
  192. ext := getExt(r)
  193. for _, extension := range options.Extensions {
  194. if ext == extension {
  195. buf, err := ioutil.ReadFile(path)
  196. if err != nil {
  197. panic(err)
  198. }
  199. name := (r[0 : len(r)-len(ext)])
  200. tmpl := t.New(filepath.ToSlash(name))
  201. // add our funcmaps
  202. for _, funcs := range options.Funcs {
  203. tmpl.Funcs(funcs)
  204. }
  205. // Bomb out if parse fails. We don't want any silent server starts.
  206. template.Must(tmpl.Funcs(helperFuncs).Parse(string(buf)))
  207. break
  208. }
  209. }
  210. return nil
  211. })
  212. return t
  213. }
  214. func getExt(s string) string {
  215. if strings.Index(s, ".") == -1 {
  216. return ""
  217. }
  218. return "." + strings.Join(strings.Split(s, ".")[1:], ".")
  219. }
  220. type renderer struct {
  221. http.ResponseWriter
  222. req *http.Request
  223. t *template.Template
  224. opt Options
  225. compiledCharset string
  226. }
  227. func (r *renderer) JSON(status int, v interface{}) {
  228. var result []byte
  229. var err error
  230. if r.opt.IndentJSON {
  231. result, err = json.MarshalIndent(v, "", " ")
  232. } else {
  233. result, err = json.Marshal(v)
  234. }
  235. if err != nil {
  236. http.Error(r, err.Error(), 500)
  237. return
  238. }
  239. // json rendered fine, write out the result
  240. r.Header().Set(ContentType, ContentJSON+r.compiledCharset)
  241. r.WriteHeader(status)
  242. if len(r.opt.PrefixJSON) > 0 {
  243. r.Write(r.opt.PrefixJSON)
  244. }
  245. r.Write(result)
  246. }
  247. func (r *renderer) HTML(status int, name string, binding interface{}, htmlOpt ...HTMLOptions) {
  248. opt := r.prepareHTMLOptions(htmlOpt)
  249. // assign a layout if there is one
  250. if len(opt.Layout) > 0 {
  251. r.addYield(name, binding)
  252. name = opt.Layout
  253. }
  254. buf, err := r.execute(name, binding)
  255. if err != nil {
  256. http.Error(r, err.Error(), http.StatusInternalServerError)
  257. return
  258. }
  259. // template rendered fine, write out the result
  260. r.Header().Set(ContentType, r.opt.HTMLContentType+r.compiledCharset)
  261. r.WriteHeader(status)
  262. io.Copy(r, buf)
  263. bufpool.Put(buf)
  264. }
  265. func (r *renderer) XML(status int, v interface{}) {
  266. var result []byte
  267. var err error
  268. if r.opt.IndentXML {
  269. result, err = xml.MarshalIndent(v, "", " ")
  270. } else {
  271. result, err = xml.Marshal(v)
  272. }
  273. if err != nil {
  274. http.Error(r, err.Error(), 500)
  275. return
  276. }
  277. // XML rendered fine, write out the result
  278. r.Header().Set(ContentType, ContentXML+r.compiledCharset)
  279. r.WriteHeader(status)
  280. if len(r.opt.PrefixXML) > 0 {
  281. r.Write(r.opt.PrefixXML)
  282. }
  283. r.Write(result)
  284. }
  285. func (r *renderer) Data(status int, v []byte) {
  286. if r.Header().Get(ContentType) == "" {
  287. r.Header().Set(ContentType, ContentBinary)
  288. }
  289. r.WriteHeader(status)
  290. r.Write(v)
  291. }
  292. func (r *renderer) Text(status int, v string) {
  293. if r.Header().Get(ContentType) == "" {
  294. r.Header().Set(ContentType, ContentText+r.compiledCharset)
  295. }
  296. r.WriteHeader(status)
  297. r.Write([]byte(v))
  298. }
  299. // Error writes the given HTTP status to the current ResponseWriter
  300. func (r *renderer) Error(status int) {
  301. r.WriteHeader(status)
  302. }
  303. func (r *renderer) Status(status int) {
  304. r.WriteHeader(status)
  305. }
  306. func (r *renderer) Redirect(location string, status ...int) {
  307. code := http.StatusFound
  308. if len(status) == 1 {
  309. code = status[0]
  310. }
  311. http.Redirect(r, r.req, location, code)
  312. }
  313. func (r *renderer) Template() *template.Template {
  314. return r.t
  315. }
  316. func (r *renderer) execute(name string, binding interface{}) (*bytes.Buffer, error) {
  317. buf := bufpool.Get()
  318. return buf, r.t.ExecuteTemplate(buf, name, binding)
  319. }
  320. func (r *renderer) addYield(name string, binding interface{}) {
  321. funcs := template.FuncMap{
  322. "yield": func() (template.HTML, error) {
  323. buf, err := r.execute(name, binding)
  324. // return safe html here since we are rendering our own template
  325. return template.HTML(buf.String()), err
  326. },
  327. "current": func() (string, error) {
  328. return name, nil
  329. },
  330. }
  331. r.t.Funcs(funcs)
  332. }
  333. func (r *renderer) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
  334. if len(htmlOpt) > 0 {
  335. return htmlOpt[0]
  336. }
  337. return HTMLOptions{
  338. Layout: r.opt.Layout,
  339. }
  340. }