| 1 | | /*! |
| 2 | | * connect-render |
| 3 | | * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com> |
| 4 | | * MIT Licensed |
| 5 | | */ |
| 6 | | |
| 7 | | /** |
| 8 | | * Module dependencies. |
| 9 | | */ |
| 10 | | |
| 11 | 3 | var fs = require('fs'); |
| 12 | 3 | var path = require('path'); |
| 13 | 3 | var http = require('http'); |
| 14 | 3 | var ejs = require('ejs'); |
| 15 | 3 | var filters = require('./filters'); |
| 16 | | |
| 17 | 3 | var settings = { |
| 18 | | root: __dirname + '/views', |
| 19 | | cache: true, |
| 20 | | layout: 'layout.html', |
| 21 | | viewExt: '', // view default extname |
| 22 | | _filters: {}, |
| 23 | | }; |
| 24 | | |
| 25 | 3 | for (var k in filters) { |
| 26 | 12 | settings._filters[k] = filters[k]; |
| 27 | | } |
| 28 | | |
| 29 | 3 | var cache = {}; |
| 30 | | |
| 31 | 3 | function _render_tpl(fn, options, callback) { |
| 32 | 25 | var str; |
| 33 | 25 | try { |
| 34 | 25 | str = fn.call(options.scope, options); |
| 35 | | } catch (err) { |
| 36 | 4 | return callback(err); |
| 37 | | } |
| 38 | 21 | callback(null, str); |
| 39 | | } |
| 40 | | |
| 41 | 3 | var reg_meta = /[\\\^$*+?{}.()|\[\]]/g; |
| 42 | 3 | var open = ejs.open || "<%"; |
| 43 | 3 | var close = ejs.close || "%>"; |
| 44 | 3 | var PARTIAL_PATTERN_RE = new RegExp(open.replace(reg_meta, "\\$&") + |
| 45 | | "[-=]\\s*partial\\((.+)\\)\\s*" + close.replace(reg_meta, "\\$&"), 'g'); |
| 46 | | /** |
| 47 | | * add support for <%- partial('view') %> function |
| 48 | | * rather than realtime compiling, this implemention simply statically 'include' the partial view file |
| 49 | | * |
| 50 | | * @param {String} data |
| 51 | | * @param {String} [viewname] view name for partial loop check. |
| 52 | | * @return {String} |
| 53 | | */ |
| 54 | 3 | function partial(data, viewname) { |
| 55 | 22 | return data.replace(PARTIAL_PATTERN_RE, function (all, view) { |
| 56 | 10 | view = view.match(/['"](.*)['"]/); // get the view name |
| 57 | 10 | if (!view || view[1] === viewname) { |
| 58 | 2 | return ""; |
| 59 | | } else { |
| 60 | 8 | var name = view[1]; |
| 61 | 8 | if (settings.viewExt) { |
| 62 | 4 | name += settings.viewExt; |
| 63 | | } |
| 64 | 8 | var viewpath = path.join(settings.root, name); |
| 65 | 8 | var tpl = ''; |
| 66 | 8 | try { |
| 67 | 8 | tpl = fs.readFileSync(viewpath, 'utf8'); |
| 68 | | } catch (e) { |
| 69 | 2 | console.error("[%s][connect-render] Error: cannot load view partial %s\n%s", new Date(), viewpath, e.stack); |
| 70 | 2 | return ""; |
| 71 | | } |
| 72 | 6 | return partial(tpl, view[1]); |
| 73 | | } |
| 74 | | }); |
| 75 | | } |
| 76 | | |
| 77 | 3 | function _render(view, options, callback) { |
| 78 | 27 | if (settings.viewExt) { |
| 79 | 12 | view += settings.viewExt; |
| 80 | | } |
| 81 | 27 | var viewpath = path.join(settings.root, view); |
| 82 | 27 | var fn = settings.cache && cache[view]; |
| 83 | 27 | if (fn) { |
| 84 | 9 | return _render_tpl(fn, options, callback); |
| 85 | | } |
| 86 | | // read template data from view file |
| 87 | 18 | fs.readFile(viewpath, 'utf8', function (err, data) { |
| 88 | 18 | if (err) { |
| 89 | 2 | return callback(err); |
| 90 | | } |
| 91 | 16 | var tpl = partial(data); |
| 92 | 16 | fn = ejs.compile(tpl, {filename: view}); |
| 93 | 16 | if (settings.cache) { |
| 94 | 16 | cache[view] = fn; |
| 95 | | } |
| 96 | 16 | _render_tpl(fn, options, callback); |
| 97 | | }); |
| 98 | | } |
| 99 | | |
| 100 | 3 | function send(res, str) { |
| 101 | 15 | var buf = new Buffer(str); |
| 102 | 15 | res.charset = res.charset || 'utf-8'; |
| 103 | 15 | res.setHeader('Content-Type', 'text/html'); |
| 104 | 15 | res.setHeader('Content-Length', buf.length); |
| 105 | 15 | res.end(buf); |
| 106 | | } |
| 107 | | |
| 108 | | /** |
| 109 | | * Render the view fill with options |
| 110 | | * |
| 111 | | * @param {String} view, view name. |
| 112 | | * @param {Object} [options=null] |
| 113 | | * - {Boolean} layout, use layout or not, default is `true`. |
| 114 | | * @return {HttpServerResponse} this |
| 115 | | */ |
| 116 | 3 | function render(view, options) { |
| 117 | 21 | var self = this; |
| 118 | 21 | options = options || {}; |
| 119 | | |
| 120 | 21 | for (var name in settings._filters) { |
| 121 | 84 | options[name] = settings._filters[name]; |
| 122 | | } |
| 123 | | |
| 124 | 21 | if (settings.helpers) { |
| 125 | 21 | for (var k in settings.helpers) { |
| 126 | 75 | var helper = settings.helpers[k]; |
| 127 | 75 | if (typeof helper === 'function') { |
| 128 | 21 | helper = helper(self.req, self); |
| 129 | | } |
| 130 | 75 | if (!options.hasOwnProperty(k)) { |
| 131 | 74 | options[k] = helper; |
| 132 | | } |
| 133 | | } |
| 134 | | } |
| 135 | | |
| 136 | 21 | if (settings.filters) { |
| 137 | 12 | for (var name in settings.filters) { |
| 138 | 12 | options[name] = settings.filters[name]; |
| 139 | | } |
| 140 | | } |
| 141 | | |
| 142 | | // add request to options |
| 143 | 21 | if (!options.request) { |
| 144 | 21 | options.request = self.req; |
| 145 | | } |
| 146 | | // render view template |
| 147 | 21 | _render(view, options, function (err, str) { |
| 148 | 21 | if (err) { |
| 149 | 4 | return self.req.next(err); |
| 150 | | } |
| 151 | 17 | var layout = typeof options.layout === 'string' ? options.layout : settings.layout; |
| 152 | 17 | if (options.layout === false || !layout) { |
| 153 | 11 | return send(self, str); |
| 154 | | } |
| 155 | | // render layout template, add view str to layout's locals.body; |
| 156 | 6 | options.body = str; |
| 157 | 6 | _render(layout, options, function (err, str) { |
| 158 | 6 | if (err) { |
| 159 | 2 | return self.req.next(err); |
| 160 | | } |
| 161 | 4 | send(self, str); |
| 162 | | }); |
| 163 | | }); |
| 164 | 21 | return this; |
| 165 | | } |
| 166 | | |
| 167 | | /** |
| 168 | | * connect-render: Template Render helper for connect |
| 169 | | * |
| 170 | | * Use case: |
| 171 | | * |
| 172 | | * var render = require('connect-render'); |
| 173 | | * var connect = require('connect'); |
| 174 | | * |
| 175 | | * connect( |
| 176 | | * render({ |
| 177 | | * root: __dirname + '/views', |
| 178 | | * cache: true, // must set `true` in production env |
| 179 | | * layout: 'layout.html', // or false for no layout |
| 180 | | * helpers: { |
| 181 | | * config: config, |
| 182 | | * sitename: 'NodeBlog Engine', |
| 183 | | * _csrf: function (req, res) { |
| 184 | | * return req.session ? req.session._csrf : ""; |
| 185 | | * }, |
| 186 | | * } |
| 187 | | * }); |
| 188 | | * ); |
| 189 | | * |
| 190 | | * res.render('index.html', { title: 'Index Page', items: items }); |
| 191 | | * |
| 192 | | * // no layout |
| 193 | | * res.render('blue.html', { items: items, layout: false }); |
| 194 | | * |
| 195 | | * @param {Object} [options={}] render options. |
| 196 | | * - {String} layout, layout name, default is `'layout.html'`. |
| 197 | | * Set `layout=''` or `layout=false` meaning no layout. |
| 198 | | * - {String} root, view files root dir. |
| 199 | | * - {Boolean} cache, cache view content or not, default is `true`. |
| 200 | | * Must set `cache = true` on production. |
| 201 | | * - {String} viewExt, view file extname, default is `''`. |
| 202 | | * - {Object} [filters={}] |
| 203 | | * - {Object} [helpers={}] |
| 204 | | * @return {Function} render middleware for `connect` |
| 205 | | */ |
| 206 | 3 | function middleware(options) { |
| 207 | 2 | options = options || {}; |
| 208 | 2 | for (var k in options) { |
| 209 | 10 | settings[k] = options[k]; |
| 210 | | } |
| 211 | 2 | return function (req, res, next) { |
| 212 | 21 | req.next = next; |
| 213 | 21 | if (!res.req) { |
| 214 | 21 | res.req = req; |
| 215 | | } |
| 216 | 21 | res.render = render; |
| 217 | 21 | next(); |
| 218 | | }; |
| 219 | | } |
| 220 | | |
| 221 | 3 | module.exports = middleware; |