diff --git a/.gitignore b/.gitignore index fdcfe6e..1712e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,30 @@ -.DS_Store -node_modules +# always ignore files +*.DS_Store +*.sublime-* .idea *.sock + +# test related, or directories generated by tests +test/actual +actual +coverage +.nyc* + +# npm +node_modules +npm-debug.log + +# yarn +yarn.lock +yarn-error.log + +# misc +_gh_pages +_draft +_drafts +bower_components +vendor +temp +tmp +TODO.md +package-lock.json diff --git a/History.md b/History.md index f1360b2..90c5fe6 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,14 @@ +0.15.0 / 2017-09-18 +=================== + + * add plates support + * add teacup support + * add liquid-node support + * add velocityjs support + * allow absolute and relative partial paths + * extend dot options + * support layouts in vash + 0.14.0 / 2016-01-24 =================== diff --git a/Readme.md b/Readme.md index cc1ac0f..5a8b170 100644 --- a/Readme.md +++ b/Readme.md @@ -32,12 +32,14 @@ - [mote](https://github.com/satchmorun/mote) [(website)](http://satchmorun.github.io/mote/) - [mustache](https://github.com/janl/mustache.js) - [nunjucks](https://github.com/mozilla/nunjucks) [(website)](https://mozilla.github.io/nunjucks) + - [plates](https://github.com/flatiron/plates) - [pug (formerly jade)](https://github.com/pugjs/pug) [(website)](http://jade-lang.com/) - [QEJS](https://github.com/jepso/QEJS) - [ractive](https://github.com/Rich-Harris/Ractive) - [react](https://github.com/facebook/react) - [slm](https://github.com/slm-lang/slm) - [swig (unmaintained)](https://github.com/paularmstrong/swig) + - [teacup](https://github.com/goodeggs/teacup) - [templayed](http://archan937.github.com/templayed.js/) - [twig](https://github.com/justjohn/twig.js) - [liquid](https://github.com/leizongmin/tinyliquid) [(website)](http://liquidmarkup.org/) @@ -103,15 +105,9 @@ cons.swig('views/page.html', { user: 'tobi' }) ## Caching - To enable or disable caching simply pass `{ cache: true/false }`. Engines _may_ use this option to cache things reading the file contents, compiled `Function`s etc. Engines which do _not_ support this may simply ignore it. All engines that consolidate.js implements I/O for will cache the file contents, ideal for production environments. - -```js -var cons = require('consolidate'); -cons.swig('views/page.html', { cache: false, user: 'tobi' }, function(err, html){ - if (err) throw err; - console.log(html); -}); -``` + To enable caching simply pass `{ cache: true }`. Engines _may_ use this option to cache things reading the file contents, compiled `Function`s etc. Engines which do _not_ support this may simply ignore it. All engines that consolidate.js implements I/O for will cache the file contents, ideal for production environments. + When using consolidate directly: `cons.swig('views/page.html', { user: 'tobi', cache:true }, callback);` + Using Express 3 or higher: `app.locals.cache = true` or set NODE_ENV to 'production' and Express will do this for you. ## Express 3.x example diff --git a/lib/consolidate.js b/lib/consolidate.js index fd3b1d5..e7119a0 100644 --- a/lib/consolidate.js +++ b/lib/consolidate.js @@ -22,6 +22,7 @@ var join = path.join; var resolve = path.resolve; var extname = path.extname; var dirname = path.dirname; +var isAbsolute = path.isAbsolute; var readCache = {}; @@ -44,6 +45,7 @@ var requires = {}; */ exports.clearCache = function(){ + readCache = {}; cacheStore = {}; }; @@ -118,11 +120,24 @@ function readPartials(path, options, fn) { function next(index) { if (index === keys.length) return fn(null); var key = keys[index]; - var filename = partials[key]; - if (filename === undefined || filename === null || filename === false) { + + var partialPath = partials[key]; + + if (partialPath === undefined || partialPath === null || partialPath === false) { return next(++index); } - var file = join(dirname(path), partials[key] + extname(path)); + + var file; + if (isAbsolute(partialPath)) { + if (extname(partialPath) !== '') { + file = partialPath; + } else { + file = join(partialPath + extname(path)); + } + } else { + file = join(dirname(path), partialPath + extname(path)); + } + read(file, options, function(err, str){ if (err) return fn(err); options.partials[key] = str; @@ -174,6 +189,28 @@ function fromStringRenderer(name) { }; } +/** + * velocity support. + */ + +exports.velocityjs = fromStringRenderer('velocityjs'); + +/** + * velocity string support. + */ + +exports.velocityjs.render = function(str, options, fn){ + return promisify(fn, function(fn) { + var engine = requires.velocityjs || (requires.velocityjs = require('velocityjs')); + try { + options.locals = options; + fn(null, engine.render(str, options).trimLeft()); + } catch (err) { + fn(err); + } + }); +}; + /** * Liquid support. */ @@ -192,27 +229,128 @@ exports.liquid = fromStringRenderer('liquid'); * `includeDir` will also become a local. */ +function _renderTinyliquid(engine, str, options, fn) { + var context = engine.newContext(); + var k; + + /** + * Note that there's a bug in the library that doesn't allow us to pass + * the locals to newContext(), hence looping through the keys: + */ + + if (options.locals) { + for (k in options.locals) { + context.setLocals(k, options.locals[k]); + } + delete options.locals; + } + + if (options.meta) { + context.setLocals('page', options.meta); + delete options.meta; + } + + /** + * Add any defined filters: + */ + + if (options.filters) { + for (k in options.filters) { + context.setFilter(k, options.filters[k]); + } + delete options.filters; + } + + /** + * Set up a callback for the include directory: + */ + + var includeDir = options.includeDir || process.cwd(); + + context.onInclude(function (name, callback) { + var extname = path.extname(name) ? '' : '.liquid'; + var filename = path.resolve(includeDir, name + extname); + + fs.readFile(filename, {encoding: 'utf8'}, function (err, data) { + if (err) return callback(err); + callback(null, engine.parse(data)); + }); + }); + delete options.includeDir; + + /** + * The custom tag functions need to have their results pushed back + * through the parser, so set up a shim before calling the provided + * callback: + */ + + var compileOptions = { + customTags: {} + }; + + if (options.customTags) { + var tagFunctions = options.customTags; + + for (k in options.customTags) { + /*Tell jshint there's no problem with having this function in the loop */ + /*jshint -W083 */ + compileOptions.customTags[k] = function (context, name, body) { + var tpl = tagFunctions[name](body.trim()); + context.astStack.push(engine.parse(tpl)); + }; + /*jshint +W083 */ + } + delete options.customTags; + } + + /** + * Now anything left in `options` becomes a local: + */ + + for (k in options) { + context.setLocals(k, options[k]); + } + + /** + * Finally, execute the template: + */ + + var tmpl = cache(context) || cache(context, engine.compile(str, compileOptions)); + tmpl(context, fn); +} + exports.liquid.render = function(str, options, fn){ return promisify(fn, function (fn) { - var engine = requires.liquid || (requires.liquid = require('tinyliquid')); + var engine = requires.liquid; + var Liquid; + try { - var context = engine.newContext(); - var k; + // set up tinyliquid engine + engine = requires.liquid = require('tinyliquid'); - /** - * Note that there's a bug in the library that doesn't allow us to pass - * the locals to newContext(), hence looping through the keys: - */ + // use tinyliquid engine + _renderTinyliquid(engine, str, options, fn); - if (options.locals){ - for (k in options.locals){ - context.setLocals(k, options.locals[k]); - } - delete options.locals; + return; + + } catch (err) { + + // set up liquid-node engine + try { + Liquid = requires.liquid = require('liquid-node'); + engine = new Liquid.Engine; + } catch (err) { + throw err; } + } + + // use liquid-node engine + try { + var locals = options.locals || {}; + if (options.meta){ - context.setLocals('page', options.meta); + locals.pages = options.meta; delete options.meta; } @@ -220,10 +358,8 @@ exports.liquid.render = function(str, options, fn){ * Add any defined filters: */ - if (options.filters){ - for (k in options.filters){ - context.setFilter(k, options.filters[k]); - } + if (options.filters) { + engine.registerFilters(options.filters); delete options.filters; } @@ -232,16 +368,7 @@ exports.liquid.render = function(str, options, fn){ */ var includeDir = options.includeDir || process.cwd(); - - context.onInclude(function (name, callback) { - var extname = path.extname(name) ? '' : '.liquid'; - var filename = path.resolve(includeDir, name + extname); - - fs.readFile(filename, {encoding: 'utf8'}, function (err, data){ - if (err) return callback(err); - callback(null, engine.parse(data)); - }); - }); + engine.fileSystem = new Liquid.LocalFileSystem(includeDir, 'liquid'); delete options.includeDir; /** @@ -258,13 +385,7 @@ exports.liquid.render = function(str, options, fn){ var tagFunctions = options.customTags; for (k in options.customTags){ - /*Tell jshint there's no problem with having this function in the loop */ - /*jshint -W083 */ - compileOptions.customTags[k] = function (context, name, body){ - var tpl = tagFunctions[name](body.trim()); - context.astStack.push(engine.parse(tpl)); - }; - /*jshint +W083 */ + engine.registerTag(k, tagFunctions[k]); } delete options.customTags; } @@ -273,16 +394,24 @@ exports.liquid.render = function(str, options, fn){ * Now anything left in `options` becomes a local: */ - for (k in options){ - context.setLocals(k, options[k]); + for (var k in options){ + locals[k] = options[k]; } /** * Finally, execute the template: */ - var tmpl = cache(context) || cache(context, engine.compile(str, compileOptions)); - tmpl(context, fn); + return engine + .parseAndRender(str, locals) + .nodeify(function (err, result) { + if (err) { + throw new Error(err); + } else { + return fn(null, result); + } + }); + } catch (err) { fn(err); } @@ -387,7 +516,12 @@ exports.dust.render = function(str, options, fn){ }; try { - var tmpl = cache(options) || cache(options, engine.compileFn(str)); + var templateName; + if (options.filename) { + templateName = options.filename.replace(new RegExp('^' + views + '/'), '').replace(new RegExp('\\.' + ext), ''); + } + + var tmpl = cache(options) || cache(options, engine.compileFn(str, templateName)); tmpl(options, fn); } catch (err) { fn(err); @@ -1032,8 +1166,12 @@ exports.dot = fromStringRenderer('dot'); exports.dot.render = function (str, options, fn) { return promisify(fn, function (fn) { var engine = requires.dot || (requires.dot = require('dot')); + var extend = (requires.extend || (requires.extend = require('util')._extend)); try { - var tmpl = cache(options) || cache(options, engine.compile(str, options && options._def)); + var settings = {}; + settings = extend(settings, engine.templateSettings); + settings = extend(settings, options ? options.dot : {}); + var tmpl = cache(options) || cache(options, engine.template(str, settings, options)); fn(null, tmpl(options)); } catch (err) { fn(err); @@ -1055,7 +1193,7 @@ exports.bracket.render = function (str, options, fn) { return promisify(fn, function (fn) { var engine = requires.bracket || (requires.bracket = require('bracket-template')); try { - var tmpl = cache(options) || cache(options, engine.default.compile(str)); + var tmpl = cache(options) || cache(options, engine.default.compile(str, options)); fn(null, tmpl(options)); } catch (err) { fn(err); @@ -1265,6 +1403,29 @@ function reactBaseTmpl(data, options){ return data; } +/** +* Plates Support. +*/ + +exports.plates = fromStringRenderer('plates'); + +/** +* Plates string support. +*/ + +exports.plates.render = function(str, options, fn) { + return promisify(fn, function (fn) { + var engine = requires.plates || (requires.plates = require('plates')); + var map = options.map || undefined; + try { + var tmpl = engine.bind(str, options, map); + fn(null, tmpl); + } catch (err) { + fn(err); + } + }); +} + /** @@ -1316,7 +1477,13 @@ function reactRenderer(type){ if (!cache(options)){ // Parsing - Code = (type === 'path') ? require(resolve(str)) : requireReactString(str); + if (type === 'path') { + var path = resolve(str) + delete require.cache[path] + Code = require(path) + } else { + Code = requireReactString(str) + } Factory = cache(options, react.createFactory(Code)); } else { @@ -1414,8 +1581,12 @@ exports.vash.render = function(str, options, fn) { engine.helpers[key] = options.helpers[key]; } } + var tmpl = cache(options) || cache(options, engine.compile(str, options)); - fn(null, tmpl(options).replace(/\n$/, '')); + tmpl(options, function sealLayout(err, ctx) { + ctx.finishLayout(); + fn(null, ctx.toString().replace(/\n$/, '')); + }); } catch (err) { fn(err); } @@ -1456,7 +1627,7 @@ exports.marko = function(path, options, fn){ try { var tmpl = cache(options) || cache(options, engine.load(path, options)); - tmpl.render(options, fn) + tmpl.renderToString(options, fn) } catch (err) { fn(err); } @@ -1471,10 +1642,11 @@ exports.marko.render = function(str, options, fn) { return promisify(fn, function (fn) { var engine = requires.marko || (requires.marko = require('marko')); options.writeToDisk = !!options.cache; + options.filename = options.filename || 'string.marko'; try { - var tmpl = cache(options) || cache(options, engine.load('string.marko', str, options)); - tmpl.render(options, fn) + var tmpl = cache(options) || cache(options, engine.load(options.filename, str, options)); + tmpl.renderToString(options, fn) } catch (err) { fn(err); } @@ -1482,7 +1654,44 @@ exports.marko.render = function(str, options, fn) { }; /** - * expose the instance of the engine + * Teacup support. + */ +exports.teacup = function(path, options, fn) { + return promisify(fn, function(fn) { + var engine = requires.teacup || (requires.teacup = require('teacup/lib/express')); + require.extensions['.teacup'] = require.extensions['.coffee']; + if (path[0] != '/') { + path = join(process.cwd(), path); + } + if (!options.cache) { + var originalFn = fn; + fn = function() { + delete require.cache[path]; + originalFn.apply(this, arguments); + }; + } + engine.renderFile(path, options, fn); + }); +}; + +/** + * Teacup string support. */ +exports.teacup.render = function(str, options, fn){ + var coffee = require('coffee-script'); + var vm = require('vm'); + var sandbox = { + module: {exports: {}}, + require: require + }; + return promisify(fn, function(fn) { + vm.runInNewContext(coffee.compile(str), sandbox); + var tmpl = sandbox.module.exports; + fn(null, tmpl(options)); + }); +} -exports.requires = requires; +/** + * expose the instance of the engine + */ + exports.requires = requires; diff --git a/package.json b/package.json index 97989a9..56f0663 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,12 @@ "description": "Template engine consolidation library", "version": "0.14.5", "author": "TJ Holowaychuk ", + "license": "MIT", + "main": "index", + "repository": { + "type": "git", + "url": "https://github.com/visionmedia/consolidate.js.git" + }, "dependencies": { "bluebird": "^3.1.1" }, @@ -11,7 +17,8 @@ "atpl": ">=0.7.6", "babel-core": "^6.7.6", "babel-preset-react": "^6.5.0", - "bracket-template": "^1.0.3", + "bracket-template": "^1.1.4", + "coffee-script": "^1.11.1", "dot": "^1.0.1", "dust": "^0.3.0", "dustjs-helpers": "^1.1.1", @@ -33,6 +40,7 @@ "jazz": "^0.0.18", "jqtpl": "~1.1.0", "just": "^0.1.8", + "liquid-node": "^2.6.1", "liquor": "^0.0.5", "lodash": "^4.0.0", "marko": "^3.12.0", @@ -40,6 +48,7 @@ "mote": "^0.2.0", "mustache": "^2.2.1", "nunjucks": "^3.0.0", + "plates": "~0.4.8", "pug": "^2.0.0-beta6", "qejs": "^3.0.5", "ractive": "^0.8.4", @@ -48,26 +57,22 @@ "should": "*", "slm": "^0.5.0", "swig": "^1.4.1", + "teacup": "^2.0.0", "templayed": ">=0.2.3", - "tinyliquid": "^0.2.22", + "tinyliquid": "^0.2.30", "toffee": "^0.1.12", "twig": "^0.10.0", "underscore": "^1.3.3", "vash": "^0.12.2", "walrus": "^0.10.1", - "whiskers": "^0.4.0" + "whiskers": "^0.4.0", + "velocityjs": "^0.8.2" }, "keywords": [ "engine", "template", "view" ], - "license": "MIT", - "main": "index", - "repository": { - "type": "git", - "url": "https://github.com/visionmedia/consolidate.js.git" - }, "scripts": { "test": "mocha" } diff --git a/test/consolidate.js b/test/consolidate.js index fbcc584..a900f8e 100644 --- a/test/consolidate.js +++ b/test/consolidate.js @@ -1,10 +1,21 @@ +var cons = require('../'); /*eslint-env node*/ /*eslint quotes: [2, "single"] */ require('./shared').test('jade'); require('./shared').test('pug'); + +// testing tinyliquid +cons.requires.liquid = require('tinyliquid'); require('./shared').test('liquid'); require('./shared/filters').test('liquid'); require('./shared/includes').test('liquid'); + +// testing liquid-node +cons.requires.liquid = require('liquid-node'); +require('./shared').test('liquid'); +require('./shared/filters').test('liquid'); +require('./shared/includes').test('liquid'); + require('./shared').test('ejs'); require('./shared').test('swig'); require('./shared').test('jazz'); @@ -19,6 +30,7 @@ require('./shared').test('hogan'); require('./shared/partials').test('hogan'); require('./shared').test('dust'); require('./shared/partials').test('dust'); +require('./shared/dust').test('dust'); require('./shared').test('handlebars'); require('./shared/partials').test('handlebars'); require('./shared/helpers').test('handlebars'); @@ -33,6 +45,7 @@ require('./shared').test('ect'); require('./shared').test('mote'); require('./shared').test('toffee'); require('./shared').test('atpl'); +require('./shared').test('plates'); require('./shared').test('templayed'); require('./shared').test('twig'); require('./shared').test('dot'); @@ -52,3 +65,5 @@ require('./shared/includes').test('arc-templates'); require('./shared/partials').test('arc-templates'); require('./shared').test('marko'); require('./shared').test('bracket'); +require('./shared').test('teacup'); +require('./shared').test('velocityjs'); diff --git a/test/fixtures/dust/user_template_name.dust b/test/fixtures/dust/user_template_name.dust new file mode 100644 index 0000000..a738f22 --- /dev/null +++ b/test/fixtures/dust/user_template_name.dust @@ -0,0 +1,2 @@ +

{user.name}

+{@templateName/} diff --git a/test/fixtures/marko/user.marko b/test/fixtures/marko/user.marko index 233b0b6..c9d7ba4 100644 --- a/test/fixtures/marko/user.marko +++ b/test/fixtures/marko/user.marko @@ -1 +1 @@ -p - ${data.user.name} +p -- ${data.user.name} diff --git a/test/fixtures/plates/user.html b/test/fixtures/plates/user.html new file mode 100644 index 0000000..8e68b1f --- /dev/null +++ b/test/fixtures/plates/user.html @@ -0,0 +1 @@ +

diff --git a/test/fixtures/plates/user.plates b/test/fixtures/plates/user.plates new file mode 100644 index 0000000..8e68b1f --- /dev/null +++ b/test/fixtures/plates/user.plates @@ -0,0 +1 @@ +

diff --git a/test/fixtures/teacup/user.teacup b/test/fixtures/teacup/user.teacup new file mode 100644 index 0000000..4328466 --- /dev/null +++ b/test/fixtures/teacup/user.teacup @@ -0,0 +1,4 @@ +{renderable, p} = require 'teacup' + +module.exports = renderable ({user}) -> + p user.name diff --git a/test/fixtures/velocityjs/user.velocityjs b/test/fixtures/velocityjs/user.velocityjs new file mode 100644 index 0000000..334ea79 --- /dev/null +++ b/test/fixtures/velocityjs/user.velocityjs @@ -0,0 +1 @@ +

$user.name

diff --git a/test/shared/dust.js b/test/shared/dust.js new file mode 100644 index 0000000..bd2503a --- /dev/null +++ b/test/shared/dust.js @@ -0,0 +1,38 @@ +/*eslint-env node, mocha */ +var cons = require('../../'); +var fs = require('fs'); + +// var should = require('should'); + +exports.test = function(name) { + var user = { name: 'Tobi' }; + + describe(name, function(){ + // Use case: return upper case string. + it('should support fetching template name from the context', function(done) { + var viewsDir = 'test/fixtures/' + name; + var templatePath = viewsDir + '/user_template_name.' + name; + var str = fs.readFileSync(templatePath).toString(); + + var locals = { + user: user, + views: viewsDir, + filename: templatePath + }; + + if (name === 'dust') { + var dust = require('dustjs-helpers'); + dust.helpers.templateName = function(chunk, context, bodies, params) { + return chunk.write(context.getTemplateName()); + }; + cons.requires.dust = dust; + } + + cons[name].render(str, locals, function(err, html){ + if (err) return done(err); + html.should.eql('

Tobi

user_template_name'); + return done(); + }); + }); + }); +}; diff --git a/test/shared/includes.js b/test/shared/includes.js index 89f2026..7224e23 100644 --- a/test/shared/includes.js +++ b/test/shared/includes.js @@ -6,7 +6,7 @@ var should = require('should'); exports.test = function(name) { var user = { name: 'Tobi' }; - describe(name, function(){ + describe(name, function() { it('should support includes', function(done) { var str = fs.readFileSync('test/fixtures/' + name + '/include.' + name).toString(); diff --git a/test/shared/index.js b/test/shared/index.js index 3f99e25..0d510fa 100644 --- a/test/shared/index.js +++ b/test/shared/index.js @@ -18,7 +18,7 @@ exports.test = function(name) { var locals = { user: user }; cons[name](path, locals, function(err, html){ if (err) return done(err); - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); done(); }); }); @@ -40,10 +40,10 @@ exports.test = function(name) { cons[name](path, locals, function(err, html){ if (err) return done(err); - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); cons[name](path, locals, function(err, html){ if (err) return done(err); - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); calls.should.equal(name === 'atpl' ? 4 : 2); done(); }); @@ -61,10 +61,10 @@ exports.test = function(name) { done(new Error('fs.readFile() called with ' + path)); }; - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); cons[name](path, locals, function(err, html){ if (err) return done(err); - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); done(); }); }); @@ -75,7 +75,7 @@ exports.test = function(name) { var locals = { user: user }; cons[name].render(str, locals, function(err, html){ if (err) return done(err); - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); done(); }); }); @@ -86,7 +86,7 @@ exports.test = function(name) { var result = cons[name](path, locals); result.then(function (html) { - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); done(); }) .catch(function (err) { @@ -100,7 +100,7 @@ exports.test = function(name) { var result = cons[name].render(str, locals); result.then(function (html) { - html.should.equal('

Tobi

'); + html.should.match(/Tobi/); done(); }) .catch(function (err) { diff --git a/test/shared/partials.js b/test/shared/partials.js index febd9aa..5982b6c 100644 --- a/test/shared/partials.js +++ b/test/shared/partials.js @@ -23,6 +23,24 @@ exports.test = function(name) { done(); }); }); + it('should support absolute path partial', function(done){ + var path = 'test/fixtures/' + name + '/partials.' + name; + var locals = {user: user, partials: {partial: __dirname + '/../../test/fixtures/' + name + '/user' }}; + cons[name](path, locals, function(err, html){ + if (err) return done(err); + html.should.equal('

Tobi

'); + done(); + }); + }); + it('should support relative path partial', function(done){ + var path = 'test/fixtures/' + name + '/partials.' + name; + var locals = {user: user, partials: {partial: '../' + name + '/user' }}; + cons[name](path, locals, function(err, html){ + if (err) return done(err); + html.should.equal('

Tobi

'); + done(); + }); + }); } else { it('should support rendering a partial', function(done){