├── .gitignore ├── .gitmodules ├── .travis.yml ├── History.md ├── Makefile ├── Readme.md ├── benchmark.js ├── ejs.js ├── ejs.min.js ├── examples ├── client.html ├── functions.ejs ├── functions.js ├── list.ejs └── list.js ├── index.js ├── lib ├── ejs.js ├── filters.js └── utils.js ├── package.json ├── support └── compile.js └── test ├── ejs.js └── fixtures ├── backslash.ejs ├── backslash.html ├── comments.ejs ├── comments.html ├── double-quote.ejs ├── double-quote.html ├── error.ejs ├── error.out ├── fail.ejs ├── include.css.ejs ├── include.css.html ├── include.ejs ├── include.html ├── includes ├── menu-item.ejs └── menu │ └── item.ejs ├── menu.ejs ├── menu.html ├── messed.ejs ├── messed.html ├── newlines.ejs ├── newlines.html ├── no.newlines.ejs ├── no.newlines.html ├── para.ejs ├── pet.ejs ├── single-quote.ejs ├── single-quote.html ├── style.css └── user.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore any vim files: 2 | *.sw[a-z] 3 | vim/.netrwhist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj/ejs/259aa234e43fcd19e4038cd87c8f259c92f2583a/.gitmodules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.11 4 | - 0.10 5 | - 0.9 6 | - 0.6 7 | - 0.8 8 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.0.0 / 2014-03-24 3 | ================== 4 | 5 | * change: escape & even if it looks like an HTML entity. Don't try to prevent double-escaping. 6 | 7 | 0.8.6 / 2014-03-21 8 | ================== 9 | 10 | * fix: Escape & even if it looks like an HTML entity. Don't try to prevent double-escaping. 11 | 12 | 0.8.5 / 2013-11-21 13 | ================== 14 | 15 | * fix: Escape apostrophe & don't over-match existing entities 16 | * fix function name changed by uglify 17 | * fixes require, closes #78 18 | 19 | 0.8.4 / 2013-05-08 20 | ================== 21 | 22 | * fix support for colons in filter arguments 23 | * fix double callback when the callback throws 24 | * rename escape option 25 | 26 | 0.8.3 / 2012-09-13 27 | ================== 28 | 29 | * allow pre-compiling into a standalone function [seanmonstar] 30 | 31 | 0.8.2 / 2012-08-16 32 | ================== 33 | 34 | * fix include "open" / "close" options. Closes #64 35 | 36 | 0.8.1 / 2012-08-11 37 | ================== 38 | 39 | * fix comments. Closes #62 [Nate Silva] 40 | 41 | 0.8.0 / 2012-07-25 42 | ================== 43 | 44 | * add `<% include file %>` support 45 | * fix wrapping of custom require in build step. Closes #57 46 | 47 | 0.7.3 / 2012-04-25 48 | ================== 49 | 50 | * Added repository to package.json [isaacs] 51 | 52 | 0.7.1 / 2012-03-26 53 | ================== 54 | 55 | * Fixed exception when using express in production caused by typo. [slaskis] 56 | 57 | 0.7.0 / 2012-03-24 58 | ================== 59 | 60 | * Added newline consumption support (`-%>`) [whoatemydomain] 61 | 62 | 0.6.1 / 2011-12-09 63 | ================== 64 | 65 | * Fixed `ejs.renderFile()` 66 | 67 | 0.6.0 / 2011-12-09 68 | ================== 69 | 70 | * Changed: you no longer need `{ locals: {} }` 71 | 72 | 0.5.0 / 2011-11-20 73 | ================== 74 | 75 | * Added express 3.x support 76 | * Added ejs.renderFile() 77 | * Added 'json' filter 78 | * Fixed tests for 0.5.x 79 | 80 | 0.4.3 / 2011-06-20 81 | ================== 82 | 83 | * Fixed stacktraces line number when used multiline js expressions [Octave] 84 | 85 | 0.4.2 / 2011-05-11 86 | ================== 87 | 88 | * Added client side support 89 | 90 | 0.4.1 / 2011-04-21 91 | ================== 92 | 93 | * Fixed error context 94 | 95 | 0.4.0 / 2011-04-21 96 | ================== 97 | 98 | * Added; ported jade's error reporting to ejs. [slaskis] 99 | 100 | 0.3.1 / 2011-02-23 101 | ================== 102 | 103 | * Fixed optional `compile()` options 104 | 105 | 0.3.0 / 2011-02-14 106 | ================== 107 | 108 | * Added 'json' filter [Yuriy Bogdanov] 109 | * Use exported version of parse function to allow monkey-patching [Anatoliy Chakkaev] 110 | 111 | 0.2.1 / 2010-10-07 112 | ================== 113 | 114 | * Added filter support 115 | * Fixed _cache_ option. ~4x performance increase 116 | 117 | 0.2.0 / 2010-08-05 118 | ================== 119 | 120 | * Added support for global tag config 121 | * Added custom tag support. Closes #5 122 | * Fixed whitespace bug. Closes #4 123 | 124 | 0.1.0 / 2010-08-04 125 | ================== 126 | 127 | * Faster implementation [ashleydev] 128 | 129 | 0.0.4 / 2010-08-02 130 | ================== 131 | 132 | * Fixed single quotes for content outside of template tags. [aniero] 133 | * Changed; `exports.compile()` now expects only "locals" 134 | 135 | 0.0.3 / 2010-07-15 136 | ================== 137 | 138 | * Fixed single quotes 139 | 140 | 0.0.2 / 2010-07-09 141 | ================== 142 | 143 | * Fixed newline preservation 144 | 145 | 0.0.1 / 2010-07-09 146 | ================== 147 | 148 | * Initial release 149 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SRC = $(shell find lib -name "*.js" -type f) 3 | UGLIFY_FLAGS = --no-mangle 4 | 5 | all: ejs.min.js 6 | 7 | test: 8 | @./node_modules/.bin/mocha \ 9 | --reporter spec 10 | 11 | ejs.js: $(SRC) 12 | @node support/compile.js $^ 13 | 14 | ejs.min.js: ejs.js 15 | @uglifyjs $(UGLIFY_FLAGS) $< > $@ \ 16 | && du ejs.min.js \ 17 | && du ejs.js 18 | 19 | clean: 20 | rm -f ejs.js 21 | rm -f ejs.min.js 22 | 23 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # EJS 2 | 3 | Embedded JavaScript templates. 4 | 5 | [![Build Status](https://travis-ci.org/visionmedia/ejs.png)](https://travis-ci.org/visionmedia/ejs) 6 | 7 | - - - 8 | NOTE: Version 2 of EJS makes some breaking changes with this version (notably, 9 | removal of the filters feature). Work on v2 is happening here: 10 | https://github.com/mde/ejs 11 | 12 | File issues for EJS v2 here: https://github.com/mde/ejs/issues 13 | - - - 14 | 15 | ## Installation 16 | 17 | $ npm install ejs 18 | 19 | ## Features 20 | 21 | * Complies with the [Express](http://expressjs.com) view system 22 | * Static caching of intermediate JavaScript 23 | * Unbuffered code for conditionals etc `<% code %>` 24 | * Escapes html by default with `<%= code %>` 25 | * Unescaped buffering with `<%- code %>` 26 | * Supports tag customization 27 | * Filter support for designer-friendly templates 28 | * Includes 29 | * Client-side support 30 | * Newline slurping with `<% code -%>` or `<% -%>` or `<%= code -%>` or `<%- code -%>` 31 | 32 | ## Example 33 | 34 | <% if (user) { %> 35 |

<%= user.name %>

36 | <% } %> 37 | 38 | ## Try out a live example now 39 | 40 | 41 | 42 | ## Usage 43 | 44 | ejs.compile(str, options); 45 | // => Function 46 | 47 | ejs.render(str, options); 48 | // => str 49 | 50 | ## Options 51 | 52 | - `cache` Compiled functions are cached, requires `filename` 53 | - `filename` Used by `cache` to key caches 54 | - `scope` Function execution context 55 | - `debug` Output generated function body 56 | - `compileDebug` When `false` no debug instrumentation is compiled 57 | - `client` Returns standalone compiled function 58 | - `open` Open tag, defaulting to "<%" 59 | - `close` Closing tag, defaulting to "%>" 60 | - * All others are template-local variables 61 | 62 | ## Includes 63 | 64 | Includes are relative to the template with the `include` statement, 65 | for example if you have "./views/users.ejs" and "./views/user/show.ejs" 66 | you would use `<% include user/show %>`. The included file(s) are literally 67 | included into the template, _no_ IO is performed after compilation, thus 68 | local variables are available to these included templates. 69 | 70 | ``` 71 | 76 | ``` 77 | 78 | ## Custom delimiters 79 | 80 | Custom delimiters can also be applied globally: 81 | 82 | var ejs = require('ejs'); 83 | ejs.open = '{{'; 84 | ejs.close = '}}'; 85 | 86 | Which would make the following a valid template: 87 | 88 |

{{= title }}

89 | 90 | ## Filters 91 | 92 | EJS conditionally supports the concept of "filters". A "filter chain" 93 | is a designer friendly api for manipulating data, without writing JavaScript. 94 | 95 | Filters can be applied by supplying the _:_ modifier, so for example if we wish to take the array `[{ name: 'tj' }, { name: 'mape' }, { name: 'guillermo' }]` and output a list of names we can do this simply with filters: 96 | 97 | Template: 98 | 99 |

<%=: users | map:'name' | join %>

100 | 101 | Output: 102 | 103 |

Tj, Mape, Guillermo

104 | 105 | Render call: 106 | 107 | ejs.render(str, { 108 | users: [ 109 | { name: 'tj' }, 110 | { name: 'mape' }, 111 | { name: 'guillermo' } 112 | ] 113 | }); 114 | 115 | Or perhaps capitalize the first user's name for display: 116 | 117 |

<%=: users | first | capitalize %>

118 | 119 | ## Filter list 120 | 121 | Currently these filters are available: 122 | 123 | - first 124 | - last 125 | - capitalize 126 | - downcase 127 | - upcase 128 | - sort 129 | - sort_by:'prop' 130 | - size 131 | - length 132 | - plus:n 133 | - minus:n 134 | - times:n 135 | - divided_by:n 136 | - join:'val' 137 | - truncate:n 138 | - truncate_words:n 139 | - replace:pattern,substitution 140 | - prepend:val 141 | - append:val 142 | - map:'prop' 143 | - reverse 144 | - get:'prop' 145 | 146 | ## Adding filters 147 | 148 | To add a filter simply add a method to the `.filters` object: 149 | 150 | ```js 151 | ejs.filters.last = function(obj) { 152 | return obj[obj.length - 1]; 153 | }; 154 | ``` 155 | 156 | ## Layouts 157 | 158 | Currently EJS has no notion of blocks, only compile-time `include`s, 159 | however you may still utilize this feature to implement "layouts" by 160 | simply including a header and footer like so: 161 | 162 | ```html 163 | <% include head %> 164 |

Title

165 |

My page

166 | <% include foot %> 167 | ``` 168 | 169 | ## client-side support 170 | 171 | include `./ejs.js` or `./ejs.min.js` and `require("ejs").compile(str)`. 172 | 173 | ## License 174 | 175 | (The MIT License) 176 | 177 | Copyright (c) 2009-2010 TJ Holowaychuk <tj@vision-media.ca> 178 | 179 | Permission is hereby granted, free of charge, to any person obtaining 180 | a copy of this software and associated documentation files (the 181 | 'Software'), to deal in the Software without restriction, including 182 | without limitation the rights to use, copy, modify, merge, publish, 183 | distribute, sublicense, and/or sell copies of the Software, and to 184 | permit persons to whom the Software is furnished to do so, subject to 185 | the following conditions: 186 | 187 | The above copyright notice and this permission notice shall be 188 | included in all copies or substantial portions of the Software. 189 | 190 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 191 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 192 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 193 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 194 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 195 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 196 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 197 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var ejs = require('./lib/ejs'), 4 | str = '<% if (foo) { %>

<%= foo %>

<% } %>', 5 | times = 50000; 6 | 7 | console.log('rendering ' + times + ' times'); 8 | 9 | var start = new Date; 10 | while (times--) { 11 | ejs.render(str, { cache: true, filename: 'test', locals: { foo: 'bar' }}); 12 | } 13 | 14 | console.log('took ' + (new Date - start) + 'ms'); -------------------------------------------------------------------------------- /ejs.js: -------------------------------------------------------------------------------- 1 | ejs = (function(){ 2 | 3 | // CommonJS require() 4 | 5 | function require(p){ 6 | if ('fs' == p) return {}; 7 | if ('path' == p) return {}; 8 | var path = require.resolve(p) 9 | , mod = require.modules[path]; 10 | if (!mod) throw new Error('failed to require "' + p + '"'); 11 | if (!mod.exports) { 12 | mod.exports = {}; 13 | mod.call(mod.exports, mod, mod.exports, require.relative(path)); 14 | } 15 | return mod.exports; 16 | } 17 | 18 | require.modules = {}; 19 | 20 | require.resolve = function (path){ 21 | var orig = path 22 | , reg = path + '.js' 23 | , index = path + '/index.js'; 24 | return require.modules[reg] && reg 25 | || require.modules[index] && index 26 | || orig; 27 | }; 28 | 29 | require.register = function (path, fn){ 30 | require.modules[path] = fn; 31 | }; 32 | 33 | require.relative = function (parent) { 34 | return function(p){ 35 | if ('.' != p.substr(0, 1)) return require(p); 36 | 37 | var path = parent.split('/') 38 | , segs = p.split('/'); 39 | path.pop(); 40 | 41 | for (var i = 0; i < segs.length; i++) { 42 | var seg = segs[i]; 43 | if ('..' == seg) path.pop(); 44 | else if ('.' != seg) path.push(seg); 45 | } 46 | 47 | return require(path.join('/')); 48 | }; 49 | }; 50 | 51 | 52 | require.register("ejs.js", function(module, exports, require){ 53 | 54 | /*! 55 | * EJS 56 | * Copyright(c) 2012 TJ Holowaychuk 57 | * MIT Licensed 58 | */ 59 | 60 | /** 61 | * Module dependencies. 62 | */ 63 | 64 | var utils = require('./utils') 65 | , path = require('path') 66 | , dirname = path.dirname 67 | , extname = path.extname 68 | , join = path.join 69 | , fs = require('fs') 70 | , read = fs.readFileSync; 71 | 72 | /** 73 | * Filters. 74 | * 75 | * @type Object 76 | */ 77 | 78 | var filters = exports.filters = require('./filters'); 79 | 80 | /** 81 | * Intermediate js cache. 82 | * 83 | * @type Object 84 | */ 85 | 86 | var cache = {}; 87 | 88 | /** 89 | * Clear intermediate js cache. 90 | * 91 | * @api public 92 | */ 93 | 94 | exports.clearCache = function(){ 95 | cache = {}; 96 | }; 97 | 98 | /** 99 | * Translate filtered code into function calls. 100 | * 101 | * @param {String} js 102 | * @return {String} 103 | * @api private 104 | */ 105 | 106 | function filtered(js) { 107 | return js.substr(1).split('|').reduce(function(js, filter){ 108 | var parts = filter.split(':') 109 | , name = parts.shift() 110 | , args = parts.join(':') || ''; 111 | if (args) args = ', ' + args; 112 | return 'filters.' + name + '(' + js + args + ')'; 113 | }); 114 | }; 115 | 116 | /** 117 | * Re-throw the given `err` in context to the 118 | * `str` of ejs, `filename`, and `lineno`. 119 | * 120 | * @param {Error} err 121 | * @param {String} str 122 | * @param {String} filename 123 | * @param {String} lineno 124 | * @api private 125 | */ 126 | 127 | function rethrow(err, str, filename, lineno){ 128 | var lines = str.split('\n') 129 | , start = Math.max(lineno - 3, 0) 130 | , end = Math.min(lines.length, lineno + 3); 131 | 132 | // Error context 133 | var context = lines.slice(start, end).map(function(line, i){ 134 | var curr = i + start + 1; 135 | return (curr == lineno ? ' >> ' : ' ') 136 | + curr 137 | + '| ' 138 | + line; 139 | }).join('\n'); 140 | 141 | // Alter exception message 142 | err.path = filename; 143 | err.message = (filename || 'ejs') + ':' 144 | + lineno + '\n' 145 | + context + '\n\n' 146 | + err.message; 147 | 148 | throw err; 149 | } 150 | 151 | /** 152 | * Parse the given `str` of ejs, returning the function body. 153 | * 154 | * @param {String} str 155 | * @return {String} 156 | * @api public 157 | */ 158 | 159 | var parse = exports.parse = function(str, options){ 160 | var options = options || {} 161 | , open = options.open || exports.open || '<%' 162 | , close = options.close || exports.close || '%>' 163 | , filename = options.filename 164 | , compileDebug = options.compileDebug !== false 165 | , buf = ""; 166 | 167 | buf += 'var buf = [];'; 168 | if (false !== options._with) buf += '\nwith (locals || {}) { (function(){ '; 169 | buf += '\n buf.push(\''; 170 | 171 | var lineno = 1; 172 | 173 | var consumeEOL = false; 174 | for (var i = 0, len = str.length; i < len; ++i) { 175 | var stri = str[i]; 176 | if (str.slice(i, open.length + i) == open) { 177 | i += open.length 178 | 179 | var prefix, postfix, line = (compileDebug ? '__stack.lineno=' : '') + lineno; 180 | switch (str[i]) { 181 | case '=': 182 | prefix = "', escape((" + line + ', '; 183 | postfix = ")), '"; 184 | ++i; 185 | break; 186 | case '-': 187 | prefix = "', (" + line + ', '; 188 | postfix = "), '"; 189 | ++i; 190 | break; 191 | default: 192 | prefix = "');" + line + ';'; 193 | postfix = "; buf.push('"; 194 | } 195 | 196 | var end = str.indexOf(close, i); 197 | 198 | if (end < 0){ 199 | throw new Error('Could not find matching close tag "' + close + '".'); 200 | } 201 | 202 | var js = str.substring(i, end) 203 | , start = i 204 | , include = null 205 | , n = 0; 206 | 207 | if ('-' == js[js.length-1]){ 208 | js = js.substring(0, js.length - 2); 209 | consumeEOL = true; 210 | } 211 | 212 | if (0 == js.trim().indexOf('include')) { 213 | var name = js.trim().slice(7).trim(); 214 | if (!filename) throw new Error('filename option is required for includes'); 215 | var path = resolveInclude(name, filename); 216 | include = read(path, 'utf8'); 217 | include = exports.parse(include, { filename: path, _with: false, open: open, close: close, compileDebug: compileDebug }); 218 | buf += "' + (function(){" + include + "})() + '"; 219 | js = ''; 220 | } 221 | 222 | while (~(n = js.indexOf("\n", n))) n++, lineno++; 223 | if (js.substr(0, 1) == ':') js = filtered(js); 224 | if (js) { 225 | if (js.lastIndexOf('//') > js.lastIndexOf('\n')) js += '\n'; 226 | buf += prefix; 227 | buf += js; 228 | buf += postfix; 229 | } 230 | i += end - start + close.length - 1; 231 | 232 | } else if (stri == "\\") { 233 | buf += "\\\\"; 234 | } else if (stri == "'") { 235 | buf += "\\'"; 236 | } else if (stri == "\r") { 237 | // ignore 238 | } else if (stri == "\n") { 239 | if (consumeEOL) { 240 | consumeEOL = false; 241 | } else { 242 | buf += "\\n"; 243 | lineno++; 244 | } 245 | } else { 246 | buf += stri; 247 | } 248 | } 249 | 250 | if (false !== options._with) buf += "'); })();\n} \nreturn buf.join('');"; 251 | else buf += "');\nreturn buf.join('');"; 252 | return buf; 253 | }; 254 | 255 | /** 256 | * Compile the given `str` of ejs into a `Function`. 257 | * 258 | * @param {String} str 259 | * @param {Object} options 260 | * @return {Function} 261 | * @api public 262 | */ 263 | 264 | var compile = exports.compile = function(str, options){ 265 | options = options || {}; 266 | var escape = options.escape || utils.escape; 267 | 268 | var input = JSON.stringify(str) 269 | , compileDebug = options.compileDebug !== false 270 | , client = options.client 271 | , filename = options.filename 272 | ? JSON.stringify(options.filename) 273 | : 'undefined'; 274 | 275 | if (compileDebug) { 276 | // Adds the fancy stack trace meta info 277 | str = [ 278 | 'var __stack = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };', 279 | rethrow.toString(), 280 | 'try {', 281 | exports.parse(str, options), 282 | '} catch (err) {', 283 | ' rethrow(err, __stack.input, __stack.filename, __stack.lineno);', 284 | '}' 285 | ].join("\n"); 286 | } else { 287 | str = exports.parse(str, options); 288 | } 289 | 290 | if (options.debug) console.log(str); 291 | if (client) str = 'escape = escape || ' + escape.toString() + ';\n' + str; 292 | 293 | try { 294 | var fn = new Function('locals, filters, escape, rethrow', str); 295 | } catch (err) { 296 | if ('SyntaxError' == err.name) { 297 | err.message += options.filename 298 | ? ' in ' + filename 299 | : ' while compiling ejs'; 300 | } 301 | throw err; 302 | } 303 | 304 | if (client) return fn; 305 | 306 | return function(locals){ 307 | return fn.call(this, locals, filters, escape, rethrow); 308 | } 309 | }; 310 | 311 | /** 312 | * Render the given `str` of ejs. 313 | * 314 | * Options: 315 | * 316 | * - `locals` Local variables object 317 | * - `cache` Compiled functions are cached, requires `filename` 318 | * - `filename` Used by `cache` to key caches 319 | * - `scope` Function execution context 320 | * - `debug` Output generated function body 321 | * - `open` Open tag, defaulting to "<%" 322 | * - `close` Closing tag, defaulting to "%>" 323 | * 324 | * @param {String} str 325 | * @param {Object} options 326 | * @return {String} 327 | * @api public 328 | */ 329 | 330 | exports.render = function(str, options){ 331 | var fn 332 | , options = options || {}; 333 | 334 | if (options.cache) { 335 | if (options.filename) { 336 | fn = cache[options.filename] || (cache[options.filename] = compile(str, options)); 337 | } else { 338 | throw new Error('"cache" option requires "filename".'); 339 | } 340 | } else { 341 | fn = compile(str, options); 342 | } 343 | 344 | options.__proto__ = options.locals; 345 | return fn.call(options.scope, options); 346 | }; 347 | 348 | /** 349 | * Render an EJS file at the given `path` and callback `fn(err, str)`. 350 | * 351 | * @param {String} path 352 | * @param {Object|Function} options or callback 353 | * @param {Function} fn 354 | * @api public 355 | */ 356 | 357 | exports.renderFile = function(path, options, fn){ 358 | var key = path + ':string'; 359 | 360 | if ('function' == typeof options) { 361 | fn = options, options = {}; 362 | } 363 | 364 | options.filename = path; 365 | 366 | var str; 367 | try { 368 | str = options.cache 369 | ? cache[key] || (cache[key] = read(path, 'utf8')) 370 | : read(path, 'utf8'); 371 | } catch (err) { 372 | fn(err); 373 | return; 374 | } 375 | fn(null, exports.render(str, options)); 376 | }; 377 | 378 | /** 379 | * Resolve include `name` relative to `filename`. 380 | * 381 | * @param {String} name 382 | * @param {String} filename 383 | * @return {String} 384 | * @api private 385 | */ 386 | 387 | function resolveInclude(name, filename) { 388 | var path = join(dirname(filename), name); 389 | var ext = extname(name); 390 | if (!ext) path += '.ejs'; 391 | return path; 392 | } 393 | 394 | // express support 395 | 396 | exports.__express = exports.renderFile; 397 | 398 | /** 399 | * Expose to require(). 400 | */ 401 | 402 | if (require.extensions) { 403 | require.extensions['.ejs'] = function (module, filename) { 404 | filename = filename || module.filename; 405 | var options = { filename: filename, client: true } 406 | , template = fs.readFileSync(filename).toString() 407 | , fn = compile(template, options); 408 | module._compile('module.exports = ' + fn.toString() + ';', filename); 409 | }; 410 | } else if (require.registerExtension) { 411 | require.registerExtension('.ejs', function(src) { 412 | return compile(src, {}); 413 | }); 414 | } 415 | 416 | }); // module: ejs.js 417 | 418 | require.register("filters.js", function(module, exports, require){ 419 | /*! 420 | * EJS - Filters 421 | * Copyright(c) 2010 TJ Holowaychuk 422 | * MIT Licensed 423 | */ 424 | 425 | /** 426 | * First element of the target `obj`. 427 | */ 428 | 429 | exports.first = function(obj) { 430 | return obj[0]; 431 | }; 432 | 433 | /** 434 | * Last element of the target `obj`. 435 | */ 436 | 437 | exports.last = function(obj) { 438 | return obj[obj.length - 1]; 439 | }; 440 | 441 | /** 442 | * Capitalize the first letter of the target `str`. 443 | */ 444 | 445 | exports.capitalize = function(str){ 446 | str = String(str); 447 | return str[0].toUpperCase() + str.substr(1, str.length); 448 | }; 449 | 450 | /** 451 | * Downcase the target `str`. 452 | */ 453 | 454 | exports.downcase = function(str){ 455 | return String(str).toLowerCase(); 456 | }; 457 | 458 | /** 459 | * Uppercase the target `str`. 460 | */ 461 | 462 | exports.upcase = function(str){ 463 | return String(str).toUpperCase(); 464 | }; 465 | 466 | /** 467 | * Sort the target `obj`. 468 | */ 469 | 470 | exports.sort = function(obj){ 471 | return Object.create(obj).sort(); 472 | }; 473 | 474 | /** 475 | * Sort the target `obj` by the given `prop` ascending. 476 | */ 477 | 478 | exports.sort_by = function(obj, prop){ 479 | return Object.create(obj).sort(function(a, b){ 480 | a = a[prop], b = b[prop]; 481 | if (a > b) return 1; 482 | if (a < b) return -1; 483 | return 0; 484 | }); 485 | }; 486 | 487 | /** 488 | * Size or length of the target `obj`. 489 | */ 490 | 491 | exports.size = exports.length = function(obj) { 492 | return obj.length; 493 | }; 494 | 495 | /** 496 | * Add `a` and `b`. 497 | */ 498 | 499 | exports.plus = function(a, b){ 500 | return Number(a) + Number(b); 501 | }; 502 | 503 | /** 504 | * Subtract `b` from `a`. 505 | */ 506 | 507 | exports.minus = function(a, b){ 508 | return Number(a) - Number(b); 509 | }; 510 | 511 | /** 512 | * Multiply `a` by `b`. 513 | */ 514 | 515 | exports.times = function(a, b){ 516 | return Number(a) * Number(b); 517 | }; 518 | 519 | /** 520 | * Divide `a` by `b`. 521 | */ 522 | 523 | exports.divided_by = function(a, b){ 524 | return Number(a) / Number(b); 525 | }; 526 | 527 | /** 528 | * Join `obj` with the given `str`. 529 | */ 530 | 531 | exports.join = function(obj, str){ 532 | return obj.join(str || ', '); 533 | }; 534 | 535 | /** 536 | * Truncate `str` to `len`. 537 | */ 538 | 539 | exports.truncate = function(str, len, append){ 540 | str = String(str); 541 | if (str.length > len) { 542 | str = str.slice(0, len); 543 | if (append) str += append; 544 | } 545 | return str; 546 | }; 547 | 548 | /** 549 | * Truncate `str` to `n` words. 550 | */ 551 | 552 | exports.truncate_words = function(str, n){ 553 | var str = String(str) 554 | , words = str.split(/ +/); 555 | return words.slice(0, n).join(' '); 556 | }; 557 | 558 | /** 559 | * Replace `pattern` with `substitution` in `str`. 560 | */ 561 | 562 | exports.replace = function(str, pattern, substitution){ 563 | return String(str).replace(pattern, substitution || ''); 564 | }; 565 | 566 | /** 567 | * Prepend `val` to `obj`. 568 | */ 569 | 570 | exports.prepend = function(obj, val){ 571 | return Array.isArray(obj) 572 | ? [val].concat(obj) 573 | : val + obj; 574 | }; 575 | 576 | /** 577 | * Append `val` to `obj`. 578 | */ 579 | 580 | exports.append = function(obj, val){ 581 | return Array.isArray(obj) 582 | ? obj.concat(val) 583 | : obj + val; 584 | }; 585 | 586 | /** 587 | * Map the given `prop`. 588 | */ 589 | 590 | exports.map = function(arr, prop){ 591 | return arr.map(function(obj){ 592 | return obj[prop]; 593 | }); 594 | }; 595 | 596 | /** 597 | * Reverse the given `obj`. 598 | */ 599 | 600 | exports.reverse = function(obj){ 601 | return Array.isArray(obj) 602 | ? obj.reverse() 603 | : String(obj).split('').reverse().join(''); 604 | }; 605 | 606 | /** 607 | * Get `prop` of the given `obj`. 608 | */ 609 | 610 | exports.get = function(obj, prop){ 611 | return obj[prop]; 612 | }; 613 | 614 | /** 615 | * Packs the given `obj` into json string 616 | */ 617 | exports.json = function(obj){ 618 | return JSON.stringify(obj); 619 | }; 620 | 621 | }); // module: filters.js 622 | 623 | require.register("utils.js", function(module, exports, require){ 624 | 625 | /*! 626 | * EJS 627 | * Copyright(c) 2010 TJ Holowaychuk 628 | * MIT Licensed 629 | */ 630 | 631 | /** 632 | * Escape the given string of `html`. 633 | * 634 | * @param {String} html 635 | * @return {String} 636 | * @api private 637 | */ 638 | 639 | exports.escape = function(html){ 640 | return String(html) 641 | .replace(/&/g, '&') 642 | .replace(//g, '>') 644 | .replace(/'/g, ''') 645 | .replace(/"/g, '"'); 646 | }; 647 | 648 | 649 | }); // module: utils.js 650 | 651 | return require("ejs"); 652 | })(); -------------------------------------------------------------------------------- /ejs.min.js: -------------------------------------------------------------------------------- 1 | ejs=function(){function require(p){if("fs"==p)return{};if("path"==p)return{};var path=require.resolve(p),mod=require.modules[path];if(!mod)throw new Error('failed to require "'+p+'"');if(!mod.exports){mod.exports={};mod.call(mod.exports,mod,mod.exports,require.relative(path))}return mod.exports}require.modules={};require.resolve=function(path){var orig=path,reg=path+".js",index=path+"/index.js";return require.modules[reg]&®||require.modules[index]&&index||orig};require.register=function(path,fn){require.modules[path]=fn};require.relative=function(parent){return function(p){if("."!=p.substr(0,1))return require(p);var path=parent.split("/"),segs=p.split("/");path.pop();for(var i=0;i> ":" ")+curr+"| "+line}).join("\n");err.path=filename;err.message=(filename||"ejs")+":"+lineno+"\n"+context+"\n\n"+err.message;throw err}var parse=exports.parse=function(str,options){var options=options||{},open=options.open||exports.open||"<%",close=options.close||exports.close||"%>",filename=options.filename,compileDebug=options.compileDebug!==false,buf="";buf+="var buf = [];";if(false!==options._with)buf+="\nwith (locals || {}) { (function(){ ";buf+="\n buf.push('";var lineno=1;var consumeEOL=false;for(var i=0,len=str.length;ijs.lastIndexOf("\n"))js+="\n";buf+=prefix;buf+=js;buf+=postfix}i+=end-start+close.length-1}else if(stri=="\\"){buf+="\\\\"}else if(stri=="'"){buf+="\\'"}else if(stri=="\r"){}else if(stri=="\n"){if(consumeEOL){consumeEOL=false}else{buf+="\\n";lineno++}}else{buf+=stri}}if(false!==options._with)buf+="'); })();\n} \nreturn buf.join('');";else buf+="');\nreturn buf.join('');";return buf};var compile=exports.compile=function(str,options){options=options||{};var escape=options.escape||utils.escape;var input=JSON.stringify(str),compileDebug=options.compileDebug!==false,client=options.client,filename=options.filename?JSON.stringify(options.filename):"undefined";if(compileDebug){str=["var __stack = { lineno: 1, input: "+input+", filename: "+filename+" };",rethrow.toString(),"try {",exports.parse(str,options),"} catch (err) {"," rethrow(err, __stack.input, __stack.filename, __stack.lineno);","}"].join("\n")}else{str=exports.parse(str,options)}if(options.debug)console.log(str);if(client)str="escape = escape || "+escape.toString()+";\n"+str;try{var fn=new Function("locals, filters, escape, rethrow",str)}catch(err){if("SyntaxError"==err.name){err.message+=options.filename?" in "+filename:" while compiling ejs"}throw err}if(client)return fn;return function(locals){return fn.call(this,locals,filters,escape,rethrow)}};exports.render=function(str,options){var fn,options=options||{};if(options.cache){if(options.filename){fn=cache[options.filename]||(cache[options.filename]=compile(str,options))}else{throw new Error('"cache" option requires "filename".')}}else{fn=compile(str,options)}options.__proto__=options.locals;return fn.call(options.scope,options)};exports.renderFile=function(path,options,fn){var key=path+":string";if("function"==typeof options){fn=options,options={}}options.filename=path;var str;try{str=options.cache?cache[key]||(cache[key]=read(path,"utf8")):read(path,"utf8")}catch(err){fn(err);return}fn(null,exports.render(str,options))};function resolveInclude(name,filename){var path=join(dirname(filename),name);var ext=extname(name);if(!ext)path+=".ejs";return path}exports.__express=exports.renderFile;if(require.extensions){require.extensions[".ejs"]=function(module,filename){filename=filename||module.filename;var options={filename:filename,client:true},template=fs.readFileSync(filename).toString(),fn=compile(template,options);module._compile("module.exports = "+fn.toString()+";",filename)}}else if(require.registerExtension){require.registerExtension(".ejs",function(src){return compile(src,{})})}});require.register("filters.js",function(module,exports,require){exports.first=function(obj){return obj[0]};exports.last=function(obj){return obj[obj.length-1]};exports.capitalize=function(str){str=String(str);return str[0].toUpperCase()+str.substr(1,str.length)};exports.downcase=function(str){return String(str).toLowerCase()};exports.upcase=function(str){return String(str).toUpperCase()};exports.sort=function(obj){return Object.create(obj).sort()};exports.sort_by=function(obj,prop){return Object.create(obj).sort(function(a,b){a=a[prop],b=b[prop];if(a>b)return 1;if(alen){str=str.slice(0,len);if(append)str+=append}return str};exports.truncate_words=function(str,n){var str=String(str),words=str.split(/ +/);return words.slice(0,n).join(" ")};exports.replace=function(str,pattern,substitution){return String(str).replace(pattern,substitution||"")};exports.prepend=function(obj,val){return Array.isArray(obj)?[val].concat(obj):val+obj};exports.append=function(obj,val){return Array.isArray(obj)?obj.concat(val):obj+val};exports.map=function(arr,prop){return arr.map(function(obj){return obj[prop]})};exports.reverse=function(obj){return Array.isArray(obj)?obj.reverse():String(obj).split("").reverse().join("")};exports.get=function(obj,prop){return obj[prop]};exports.json=function(obj){return JSON.stringify(obj)}});require.register("utils.js",function(module,exports,require){exports.escape=function(html){return String(html).replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""")}});return require("ejs")}(); -------------------------------------------------------------------------------- /examples/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/functions.ejs: -------------------------------------------------------------------------------- 1 |

Users

2 | 3 | <% function user(user) { %> 4 |
  • <%= user.name %> is a <%= user.age %> year old <%= user.species %>.
  • 5 | <% } %> 6 | 7 |
      8 | <% users.map(user) %> 9 |
    -------------------------------------------------------------------------------- /examples/functions.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var ejs = require('../') 7 | , fs = require('fs') 8 | , path = __dirname + '/functions.ejs' 9 | , str = fs.readFileSync(path, 'utf8'); 10 | 11 | var users = []; 12 | 13 | users.push({ name: 'Tobi', age: 2, species: 'ferret' }) 14 | users.push({ name: 'Loki', age: 2, species: 'ferret' }) 15 | users.push({ name: 'Jane', age: 6, species: 'ferret' }) 16 | 17 | var ret = ejs.render(str, { 18 | users: users, 19 | filename: path 20 | }); 21 | 22 | console.log(ret); -------------------------------------------------------------------------------- /examples/list.ejs: -------------------------------------------------------------------------------- 1 | <% if (names.length) { %> 2 |
      3 | <% names.forEach(function(name){ %> 4 |
    • '><%= name %>
    • 5 | <% }) %> 6 |
    7 | <% } %> -------------------------------------------------------------------------------- /examples/list.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var ejs = require('../') 7 | , fs = require('fs') 8 | , str = fs.readFileSync(__dirname + '/list.ejs', 'utf8'); 9 | 10 | var ret = ejs.render(str, { 11 | names: ['foo', 'bar', 'baz'] 12 | }); 13 | 14 | console.log(ret); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/ejs'); -------------------------------------------------------------------------------- /lib/ejs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * EJS 3 | * Copyright(c) 2012 TJ Holowaychuk 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('./utils') 12 | , path = require('path') 13 | , dirname = path.dirname 14 | , extname = path.extname 15 | , join = path.join 16 | , fs = require('fs') 17 | , read = fs.readFileSync; 18 | 19 | /** 20 | * Filters. 21 | * 22 | * @type Object 23 | */ 24 | 25 | var filters = exports.filters = require('./filters'); 26 | 27 | /** 28 | * Intermediate js cache. 29 | * 30 | * @type Object 31 | */ 32 | 33 | var cache = {}; 34 | 35 | /** 36 | * Clear intermediate js cache. 37 | * 38 | * @api public 39 | */ 40 | 41 | exports.clearCache = function(){ 42 | cache = {}; 43 | }; 44 | 45 | /** 46 | * Translate filtered code into function calls. 47 | * 48 | * @param {String} js 49 | * @return {String} 50 | * @api private 51 | */ 52 | 53 | function filtered(js) { 54 | return js.substr(1).split('|').reduce(function(js, filter){ 55 | var parts = filter.split(':') 56 | , name = parts.shift() 57 | , args = parts.join(':') || ''; 58 | if (args) args = ', ' + args; 59 | return 'filters.' + name + '(' + js + args + ')'; 60 | }); 61 | }; 62 | 63 | /** 64 | * Re-throw the given `err` in context to the 65 | * `str` of ejs, `filename`, and `lineno`. 66 | * 67 | * @param {Error} err 68 | * @param {String} str 69 | * @param {String} filename 70 | * @param {String} lineno 71 | * @api private 72 | */ 73 | 74 | function rethrow(err, str, filename, lineno){ 75 | var lines = str.split('\n') 76 | , start = Math.max(lineno - 3, 0) 77 | , end = Math.min(lines.length, lineno + 3); 78 | 79 | // Error context 80 | var context = lines.slice(start, end).map(function(line, i){ 81 | var curr = i + start + 1; 82 | return (curr == lineno ? ' >> ' : ' ') 83 | + curr 84 | + '| ' 85 | + line; 86 | }).join('\n'); 87 | 88 | // Alter exception message 89 | err.path = filename; 90 | err.message = (filename || 'ejs') + ':' 91 | + lineno + '\n' 92 | + context + '\n\n' 93 | + err.message; 94 | 95 | throw err; 96 | } 97 | 98 | /** 99 | * Parse the given `str` of ejs, returning the function body. 100 | * 101 | * @param {String} str 102 | * @return {String} 103 | * @api public 104 | */ 105 | 106 | var parse = exports.parse = function(str, options){ 107 | var options = options || {} 108 | , open = options.open || exports.open || '<%' 109 | , close = options.close || exports.close || '%>' 110 | , filename = options.filename 111 | , compileDebug = options.compileDebug !== false 112 | , buf = ""; 113 | 114 | buf += 'var buf = [];'; 115 | if (false !== options._with) buf += '\nwith (locals || {}) { (function(){ '; 116 | buf += '\n buf.push(\''; 117 | 118 | var lineno = 1; 119 | 120 | var consumeEOL = false; 121 | for (var i = 0, len = str.length; i < len; ++i) { 122 | var stri = str[i]; 123 | if (str.slice(i, open.length + i) == open) { 124 | i += open.length 125 | 126 | var prefix, postfix, line = (compileDebug ? '__stack.lineno=' : '') + lineno; 127 | switch (str[i]) { 128 | case '=': 129 | prefix = "', escape((" + line + ', '; 130 | postfix = ")), '"; 131 | ++i; 132 | break; 133 | case '-': 134 | prefix = "', (" + line + ', '; 135 | postfix = "), '"; 136 | ++i; 137 | break; 138 | default: 139 | prefix = "');" + line + ';'; 140 | postfix = "; buf.push('"; 141 | } 142 | 143 | var end = str.indexOf(close, i); 144 | 145 | if (end < 0){ 146 | throw new Error('Could not find matching close tag "' + close + '".'); 147 | } 148 | 149 | var js = str.substring(i, end) 150 | , start = i 151 | , include = null 152 | , n = 0; 153 | 154 | if ('-' == js[js.length-1]){ 155 | js = js.substring(0, js.length - 2); 156 | consumeEOL = true; 157 | } 158 | 159 | if (0 == js.trim().indexOf('include')) { 160 | var name = js.trim().slice(7).trim(); 161 | if (!filename) throw new Error('filename option is required for includes'); 162 | var path = resolveInclude(name, filename); 163 | include = read(path, 'utf8'); 164 | include = exports.parse(include, { filename: path, _with: false, open: open, close: close, compileDebug: compileDebug }); 165 | buf += "' + (function(){" + include + "})() + '"; 166 | js = ''; 167 | } 168 | 169 | while (~(n = js.indexOf("\n", n))) n++, lineno++; 170 | 171 | switch(js.substr(0, 1)) { 172 | case ':': 173 | js = filtered(js); 174 | break; 175 | case '%': 176 | js = " buf.push('<%" + js.substring(1).replace(/'/g, "\\'") + "%>');"; 177 | break; 178 | case '#': 179 | js = ""; 180 | break; 181 | } 182 | 183 | if (js) { 184 | if (js.lastIndexOf('//') > js.lastIndexOf('\n')) js += '\n'; 185 | buf += prefix; 186 | buf += js; 187 | buf += postfix; 188 | } 189 | i += end - start + close.length - 1; 190 | 191 | } else if (stri == "\\") { 192 | buf += "\\\\"; 193 | } else if (stri == "'") { 194 | buf += "\\'"; 195 | } else if (stri == "\r") { 196 | // ignore 197 | } else if (stri == "\n") { 198 | if (consumeEOL) { 199 | consumeEOL = false; 200 | } else { 201 | buf += "\\n"; 202 | lineno++; 203 | } 204 | } else { 205 | buf += stri; 206 | } 207 | } 208 | 209 | if (false !== options._with) buf += "'); })();\n} \nreturn buf.join('');"; 210 | else buf += "');\nreturn buf.join('');"; 211 | return buf; 212 | }; 213 | 214 | /** 215 | * Compile the given `str` of ejs into a `Function`. 216 | * 217 | * @param {String} str 218 | * @param {Object} options 219 | * @return {Function} 220 | * @api public 221 | */ 222 | 223 | var compile = exports.compile = function(str, options){ 224 | options = options || {}; 225 | var escape = options.escape || utils.escape; 226 | 227 | var input = JSON.stringify(str) 228 | , compileDebug = options.compileDebug !== false 229 | , client = options.client 230 | , filename = options.filename 231 | ? JSON.stringify(options.filename) 232 | : 'undefined'; 233 | 234 | if (compileDebug) { 235 | // Adds the fancy stack trace meta info 236 | str = [ 237 | 'var __stack = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };', 238 | rethrow.toString(), 239 | 'try {', 240 | exports.parse(str, options), 241 | '} catch (err) {', 242 | ' rethrow(err, __stack.input, __stack.filename, __stack.lineno);', 243 | '}' 244 | ].join("\n"); 245 | } else { 246 | str = exports.parse(str, options); 247 | } 248 | 249 | if (options.debug) console.log(str); 250 | if (client) str = 'escape = escape || ' + escape.toString() + ';\n' + str; 251 | 252 | try { 253 | var fn = new Function('locals, filters, escape, rethrow', str); 254 | } catch (err) { 255 | if ('SyntaxError' == err.name) { 256 | err.message += options.filename 257 | ? ' in ' + filename 258 | : ' while compiling ejs'; 259 | } 260 | throw err; 261 | } 262 | 263 | if (client) return fn; 264 | 265 | return function(locals){ 266 | return fn.call(this, locals, filters, escape, rethrow); 267 | } 268 | }; 269 | 270 | /** 271 | * Render the given `str` of ejs. 272 | * 273 | * Options: 274 | * 275 | * - `locals` Local variables object 276 | * - `cache` Compiled functions are cached, requires `filename` 277 | * - `filename` Used by `cache` to key caches 278 | * - `scope` Function execution context 279 | * - `debug` Output generated function body 280 | * - `open` Open tag, defaulting to "<%" 281 | * - `close` Closing tag, defaulting to "%>" 282 | * 283 | * @param {String} str 284 | * @param {Object} options 285 | * @return {String} 286 | * @api public 287 | */ 288 | 289 | exports.render = function(str, options){ 290 | var fn 291 | , options = options || {}; 292 | 293 | if (options.cache) { 294 | if (options.filename) { 295 | fn = cache[options.filename] || (cache[options.filename] = compile(str, options)); 296 | } else { 297 | throw new Error('"cache" option requires "filename".'); 298 | } 299 | } else { 300 | fn = compile(str, options); 301 | } 302 | 303 | options.__proto__ = options.locals; 304 | return fn.call(options.scope, options); 305 | }; 306 | 307 | /** 308 | * Render an EJS file at the given `path` and callback `fn(err, str)`. 309 | * 310 | * @param {String} path 311 | * @param {Object|Function} options or callback 312 | * @param {Function} fn 313 | * @api public 314 | */ 315 | 316 | exports.renderFile = function(path, options, fn){ 317 | var key = path + ':string'; 318 | 319 | if ('function' == typeof options) { 320 | fn = options, options = {}; 321 | } 322 | 323 | options.filename = path; 324 | 325 | var str; 326 | try { 327 | str = options.cache 328 | ? cache[key] || (cache[key] = read(path, 'utf8')) 329 | : read(path, 'utf8'); 330 | } catch (err) { 331 | fn(err); 332 | return; 333 | } 334 | fn(null, exports.render(str, options)); 335 | }; 336 | 337 | /** 338 | * Resolve include `name` relative to `filename`. 339 | * 340 | * @param {String} name 341 | * @param {String} filename 342 | * @return {String} 343 | * @api private 344 | */ 345 | 346 | function resolveInclude(name, filename) { 347 | var path = join(dirname(filename), name); 348 | var ext = extname(name); 349 | if (!ext) path += '.ejs'; 350 | return path; 351 | } 352 | 353 | // express support 354 | 355 | exports.__express = exports.renderFile; 356 | 357 | /** 358 | * Expose to require(). 359 | */ 360 | 361 | if (require.extensions) { 362 | require.extensions['.ejs'] = function (module, filename) { 363 | filename = filename || module.filename; 364 | var options = { filename: filename, client: true } 365 | , template = fs.readFileSync(filename).toString() 366 | , fn = compile(template, options); 367 | module._compile('module.exports = ' + fn.toString() + ';', filename); 368 | }; 369 | } else if (require.registerExtension) { 370 | require.registerExtension('.ejs', function(src) { 371 | return compile(src, {}); 372 | }); 373 | } 374 | -------------------------------------------------------------------------------- /lib/filters.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * EJS - Filters 3 | * Copyright(c) 2010 TJ Holowaychuk 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * First element of the target `obj`. 9 | */ 10 | 11 | exports.first = function(obj) { 12 | return obj[0]; 13 | }; 14 | 15 | /** 16 | * Last element of the target `obj`. 17 | */ 18 | 19 | exports.last = function(obj) { 20 | return obj[obj.length - 1]; 21 | }; 22 | 23 | /** 24 | * Capitalize the first letter of the target `str`. 25 | */ 26 | 27 | exports.capitalize = function(str){ 28 | str = String(str); 29 | return str[0].toUpperCase() + str.substr(1, str.length); 30 | }; 31 | 32 | /** 33 | * Downcase the target `str`. 34 | */ 35 | 36 | exports.downcase = function(str){ 37 | return String(str).toLowerCase(); 38 | }; 39 | 40 | /** 41 | * Uppercase the target `str`. 42 | */ 43 | 44 | exports.upcase = function(str){ 45 | return String(str).toUpperCase(); 46 | }; 47 | 48 | /** 49 | * Sort the target `obj`. 50 | */ 51 | 52 | exports.sort = function(obj){ 53 | return Object.create(obj).sort(); 54 | }; 55 | 56 | /** 57 | * Sort the target `obj` by the given `prop` ascending. 58 | */ 59 | 60 | exports.sort_by = function(obj, prop){ 61 | return Object.create(obj).sort(function(a, b){ 62 | a = a[prop], b = b[prop]; 63 | if (a > b) return 1; 64 | if (a < b) return -1; 65 | return 0; 66 | }); 67 | }; 68 | 69 | /** 70 | * Size or length of the target `obj`. 71 | */ 72 | 73 | exports.size = exports.length = function(obj) { 74 | return obj.length; 75 | }; 76 | 77 | /** 78 | * Add `a` and `b`. 79 | */ 80 | 81 | exports.plus = function(a, b){ 82 | return Number(a) + Number(b); 83 | }; 84 | 85 | /** 86 | * Subtract `b` from `a`. 87 | */ 88 | 89 | exports.minus = function(a, b){ 90 | return Number(a) - Number(b); 91 | }; 92 | 93 | /** 94 | * Multiply `a` by `b`. 95 | */ 96 | 97 | exports.times = function(a, b){ 98 | return Number(a) * Number(b); 99 | }; 100 | 101 | /** 102 | * Divide `a` by `b`. 103 | */ 104 | 105 | exports.divided_by = function(a, b){ 106 | return Number(a) / Number(b); 107 | }; 108 | 109 | /** 110 | * Join `obj` with the given `str`. 111 | */ 112 | 113 | exports.join = function(obj, str){ 114 | return obj.join(str || ', '); 115 | }; 116 | 117 | /** 118 | * Truncate `str` to `len`. 119 | */ 120 | 121 | exports.truncate = function(str, len, append){ 122 | str = String(str); 123 | if (str.length > len) { 124 | str = str.slice(0, len); 125 | if (append) str += append; 126 | } 127 | return str; 128 | }; 129 | 130 | /** 131 | * Truncate `str` to `n` words. 132 | */ 133 | 134 | exports.truncate_words = function(str, n){ 135 | var str = String(str) 136 | , words = str.split(/ +/); 137 | return words.slice(0, n).join(' '); 138 | }; 139 | 140 | /** 141 | * Replace `pattern` with `substitution` in `str`. 142 | */ 143 | 144 | exports.replace = function(str, pattern, substitution){ 145 | return String(str).replace(pattern, substitution || ''); 146 | }; 147 | 148 | /** 149 | * Prepend `val` to `obj`. 150 | */ 151 | 152 | exports.prepend = function(obj, val){ 153 | return Array.isArray(obj) 154 | ? [val].concat(obj) 155 | : val + obj; 156 | }; 157 | 158 | /** 159 | * Append `val` to `obj`. 160 | */ 161 | 162 | exports.append = function(obj, val){ 163 | return Array.isArray(obj) 164 | ? obj.concat(val) 165 | : obj + val; 166 | }; 167 | 168 | /** 169 | * Map the given `prop`. 170 | */ 171 | 172 | exports.map = function(arr, prop){ 173 | return arr.map(function(obj){ 174 | return obj[prop]; 175 | }); 176 | }; 177 | 178 | /** 179 | * Reverse the given `obj`. 180 | */ 181 | 182 | exports.reverse = function(obj){ 183 | return Array.isArray(obj) 184 | ? obj.reverse() 185 | : String(obj).split('').reverse().join(''); 186 | }; 187 | 188 | /** 189 | * Get `prop` of the given `obj`. 190 | */ 191 | 192 | exports.get = function(obj, prop){ 193 | return obj[prop]; 194 | }; 195 | 196 | /** 197 | * Packs the given `obj` into json string 198 | */ 199 | exports.json = function(obj){ 200 | return JSON.stringify(obj); 201 | }; 202 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * EJS 4 | * Copyright(c) 2010 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Escape the given string of `html`. 10 | * 11 | * @param {String} html 12 | * @return {String} 13 | * @api private 14 | */ 15 | 16 | exports.escape = function(html){ 17 | return String(html) 18 | .replace(/&/g, '&') 19 | .replace(//g, '>') 21 | .replace(/'/g, ''') 22 | .replace(/"/g, '"'); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ejs", 3 | "description": "Embedded JavaScript templates", 4 | "version": "1.0.0", 5 | "author": "TJ Holowaychuk ", 6 | "keywords": ["template", "engine", "ejs"], 7 | "devDependencies": { 8 | "mocha": "*", 9 | "should": "*" 10 | }, 11 | "main": "./lib/ejs.js", 12 | "repository": "git://github.com/visionmedia/ejs.git", 13 | "scripts": { 14 | "test": "mocha --require should --reporter spec" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /support/compile.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var fs = require('fs'); 7 | 8 | /** 9 | * Arguments. 10 | */ 11 | 12 | var args = process.argv.slice(2) 13 | , pending = args.length 14 | , files = {}; 15 | 16 | console.log(''); 17 | 18 | // parse arguments 19 | 20 | args.forEach(function(file){ 21 | var mod = file.replace('lib/', ''); 22 | fs.readFile(file, 'utf8', function(err, js){ 23 | if (err) throw err; 24 | console.log(' \033[90mcompile : \033[0m\033[36m%s\033[0m', file); 25 | files[file] = parse(js); 26 | --pending || compile(); 27 | }); 28 | }); 29 | 30 | /** 31 | * Parse the given `js`. 32 | */ 33 | 34 | function parse(js) { 35 | return parseInheritance(parseConditionals(js)); 36 | } 37 | 38 | /** 39 | * Parse __proto__. 40 | */ 41 | 42 | function parseInheritance(js) { 43 | return js 44 | .replace(/^ *(\w+)\.prototype\.__proto__ * = *(\w+)\.prototype *;?/gm, function(_, child, parent){ 45 | return child + '.prototype = new ' + parent + ';\n' 46 | + child + '.prototype.constructor = '+ child + ';\n'; 47 | }); 48 | } 49 | 50 | /** 51 | * Parse the given `js`, currently supporting: 52 | * 53 | * 'if' ['node' | 'browser'] 54 | * 'end' 55 | * 56 | */ 57 | 58 | function parseConditionals(js) { 59 | var lines = js.split('\n') 60 | , len = lines.length 61 | , buffer = true 62 | , browser = false 63 | , buf = [] 64 | , line 65 | , cond; 66 | 67 | for (var i = 0; i < len; ++i) { 68 | line = lines[i]; 69 | if (/^ *\/\/ *if *(node|browser)/gm.exec(line)) { 70 | cond = RegExp.$1; 71 | buffer = browser = 'browser' == cond; 72 | } else if (/^ *\/\/ *end/.test(line)) { 73 | buffer = true; 74 | browser = false; 75 | } else if (browser) { 76 | buf.push(line.replace(/^( *)\/\//, '$1')); 77 | } else if (buffer) { 78 | buf.push(line); 79 | } 80 | } 81 | 82 | return buf.join('\n'); 83 | } 84 | 85 | /** 86 | * Compile the files. 87 | */ 88 | 89 | function compile() { 90 | var buf = ''; 91 | buf += 'ejs = (function(){\n'; 92 | buf += '\n// CommonJS require()\n\n'; 93 | buf += browser.require + '\n\n'; 94 | buf += 'require.modules = {};\n\n'; 95 | buf += 'require.resolve = ' + browser.resolve + ';\n\n'; 96 | buf += 'require.register = ' + browser.register + ';\n\n'; 97 | buf += 'require.relative = ' + browser.relative + ';\n\n'; 98 | args.forEach(function(file){ 99 | var js = files[file]; 100 | file = file.replace('lib/', ''); 101 | buf += '\nrequire.register("' + file + '", function(module, exports, require){\n'; 102 | buf += js; 103 | buf += '\n}); // module: ' + file + '\n'; 104 | }); 105 | buf += '\n return require("ejs");\n})();'; 106 | fs.writeFile('ejs.js', buf, function(err){ 107 | if (err) throw err; 108 | console.log(' \033[90m create : \033[0m\033[36m%s\033[0m', 'ejs.js'); 109 | console.log(); 110 | }); 111 | } 112 | 113 | // refactored version of weepy's 114 | // https://github.com/weepy/brequire/blob/master/browser/brequire.js 115 | 116 | var browser = { 117 | 118 | /** 119 | * Require a module. 120 | */ 121 | 122 | require: function require(p){ 123 | if ('fs' == p) return {}; 124 | if ('path' == p) return {}; 125 | var path = require.resolve(p) 126 | , mod = require.modules[path]; 127 | if (!mod) throw new Error('failed to require "' + p + '"'); 128 | if (!mod.exports) { 129 | mod.exports = {}; 130 | mod.call(mod.exports, mod, mod.exports, require.relative(path)); 131 | } 132 | return mod.exports; 133 | }, 134 | 135 | /** 136 | * Resolve module path. 137 | */ 138 | 139 | resolve: function(path){ 140 | var orig = path 141 | , reg = path + '.js' 142 | , index = path + '/index.js'; 143 | return require.modules[reg] && reg 144 | || require.modules[index] && index 145 | || orig; 146 | }, 147 | 148 | /** 149 | * Return relative require(). 150 | */ 151 | 152 | relative: function(parent) { 153 | return function(p){ 154 | if ('.' != p.substr(0, 1)) return require(p); 155 | 156 | var path = parent.split('/') 157 | , segs = p.split('/'); 158 | path.pop(); 159 | 160 | for (var i = 0; i < segs.length; i++) { 161 | var seg = segs[i]; 162 | if ('..' == seg) path.pop(); 163 | else if ('.' != seg) path.push(seg); 164 | } 165 | 166 | return require(path.join('/')); 167 | }; 168 | }, 169 | 170 | /** 171 | * Register a module. 172 | */ 173 | 174 | register: function(path, fn){ 175 | require.modules[path] = fn; 176 | } 177 | }; -------------------------------------------------------------------------------- /test/ejs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var ejs = require('..') 6 | , fs = require('fs') 7 | , read = fs.readFileSync 8 | , assert = require('should'); 9 | 10 | /** 11 | * Apparently someone wrote a test depending on a method 12 | * not included in the specified version of Should 13 | */ 14 | assert.Assertion.add('include', function (param) { 15 | if (this.obj.indexOf(param) > -1) { 16 | } 17 | else { 18 | throw new Error('Substring "' + param + 19 | '" not found in string "' + this.obj + '"'); 20 | } 21 | }); 22 | 23 | 24 | /** 25 | * Load fixture `name`. 26 | */ 27 | 28 | function fixture(name) { 29 | return read('test/fixtures/' + name, 'utf8').replace(/\r/g, ''); 30 | } 31 | 32 | /** 33 | * User fixtures. 34 | */ 35 | 36 | var users = []; 37 | users.push({ name: 'tobi' }); 38 | users.push({ name: 'loki' }); 39 | users.push({ name: 'jane' }); 40 | 41 | describe('ejs.compile(str, options)', function(){ 42 | it('should compile to a function', function(){ 43 | var fn = ejs.compile('

    yay

    '); 44 | fn().should.equal('

    yay

    '); 45 | }) 46 | 47 | it('should throw if there are syntax errors', function(){ 48 | try { 49 | ejs.compile(fixture('fail.ejs')); 50 | } catch (err) { 51 | err.message.should.include('compiling ejs'); 52 | 53 | try { 54 | ejs.compile(fixture('fail.ejs'), { filename: 'fail.ejs' }); 55 | } catch (err) { 56 | err.message.should.include('fail.ejs'); 57 | return; 58 | } 59 | } 60 | 61 | assert(false, 'compiling a file with invalid syntax should throw an exception'); 62 | }) 63 | 64 | it('should allow customizing delimiters', function(){ 65 | var fn = ejs.compile('

    {= name }

    ', { open: '{', close: '}' }); 66 | fn({ name: 'tobi' }).should.equal('

    tobi

    '); 67 | 68 | var fn = ejs.compile('

    ::= name ::

    ', { open: '::', close: '::' }); 69 | fn({ name: 'tobi' }).should.equal('

    tobi

    '); 70 | 71 | var fn = ejs.compile('

    (= name )

    ', { open: '(', close: ')' }); 72 | fn({ name: 'tobi' }).should.equal('

    tobi

    '); 73 | }) 74 | 75 | it('should default to using ejs.open and ejs.close', function(){ 76 | ejs.open = '{'; 77 | ejs.close = '}'; 78 | var fn = ejs.compile('

    {= name }

    '); 79 | fn({ name: 'tobi' }).should.equal('

    tobi

    '); 80 | 81 | var fn = ejs.compile('

    |= name |

    ', { open: '|', close: '|' }); 82 | fn({ name: 'tobi' }).should.equal('

    tobi

    '); 83 | delete ejs.open; 84 | delete ejs.close; 85 | }) 86 | 87 | it('should have a working client option', function(){ 88 | var fn = ejs.compile('

    <%= foo %>

    ', { client: true }); 89 | var str = fn.toString(); 90 | eval('var preFn = ' + str); 91 | preFn({ foo: 'bar' }).should.equal('

    bar

    '); 92 | }) 93 | }) 94 | 95 | describe('ejs.render(str, options)', function(){ 96 | it('should render the template', function(){ 97 | ejs.render('

    yay

    ') 98 | .should.equal('

    yay

    '); 99 | }) 100 | 101 | it('should accept locals', function(){ 102 | ejs.render('

    <%= name %>

    ', { name: 'tobi' }) 103 | .should.equal('

    tobi

    '); 104 | }) 105 | }) 106 | 107 | describe('ejs.renderFile(path, options, fn)', function(){ 108 | it('should render a file', function(done){ 109 | ejs.renderFile('test/fixtures/para.ejs', function(err, html){ 110 | if (err) return done(err); 111 | html.should.equal('

    hey

    '); 112 | done(); 113 | }); 114 | }) 115 | 116 | it('should accept locals', function(done){ 117 | var options = { name: 'tj', open: '{', close: '}' }; 118 | ejs.renderFile('test/fixtures/user.ejs', options, function(err, html){ 119 | if (err) return done(err); 120 | html.should.equal('

    tj

    '); 121 | done(); 122 | }); 123 | }) 124 | 125 | it('should not catch err threw by callback', function(done){ 126 | var options = { name: 'tj', open: '{', close: '}' }; 127 | var counter = 0; 128 | try { 129 | ejs.renderFile('test/fixtures/user.ejs', options, function(err, html){ 130 | counter++; 131 | if (err) { 132 | err.message.should.not.equal('Exception in callback'); 133 | return done(err); 134 | } 135 | throw new Error('Exception in callback'); 136 | }); 137 | } catch (err) { 138 | counter.should.equal(1); 139 | err.message.should.equal('Exception in callback'); 140 | done(); 141 | } 142 | }) 143 | }) 144 | 145 | describe('<%=', function(){ 146 | 147 | it('should escape &