1'use strict'; 2 3var fs = require('fs'); 4var assert = require('assert'); 5var Promise = require('promise'); 6var isPromise = require('is-promise'); 7 8var tr = (module.exports = function (transformer) { 9 return new Transformer(transformer); 10}); 11tr.Transformer = Transformer; 12tr.normalizeFn = normalizeFn; 13tr.normalizeFnAsync = normalizeFnAsync; 14tr.normalize = normalize; 15tr.normalizeAsync = normalizeAsync; 16if (fs.readFile) { 17 tr.readFile = Promise.denodeify(fs.readFile); 18 tr.readFileSync = fs.readFileSync; 19} else { 20 tr.readFile = function () { throw new Error('fs.readFile unsupported'); }; 21 tr.readFileSync = function () { throw new Error('fs.readFileSync unsupported'); }; 22} 23 24function normalizeFn(result) { 25 if (typeof result === 'function') { 26 return {fn: result, dependencies: []}; 27 } else if (result && typeof result === 'object' && typeof result.fn === 'function') { 28 if ('dependencies' in result) { 29 if (!Array.isArray(result.dependencies)) { 30 throw new Error('Result should have a dependencies property that is an array'); 31 } 32 } else { 33 result.dependencies = []; 34 } 35 return result; 36 } else { 37 throw new Error('Invalid result object from transform.'); 38 } 39} 40function normalizeFnAsync(result, cb) { 41 return Promise.resolve(result).then(function (result) { 42 if (result && isPromise(result.fn)) { 43 return result.fn.then(function (fn) { 44 result.fn = fn; 45 return result; 46 }); 47 } 48 return result; 49 }).then(tr.normalizeFn).nodeify(cb); 50} 51function normalize(result) { 52 if (typeof result === 'string') { 53 return {body: result, dependencies: []}; 54 } else if (result && typeof result === 'object' && typeof result.body === 'string') { 55 if ('dependencies' in result) { 56 if (!Array.isArray(result.dependencies)) { 57 throw new Error('Result should have a dependencies property that is an array'); 58 } 59 } else { 60 result.dependencies = []; 61 } 62 return result; 63 } else { 64 throw new Error('Invalid result object from transform.'); 65 } 66} 67function normalizeAsync(result, cb) { 68 return Promise.resolve(result).then(function (result) { 69 if (result && isPromise(result.body)) { 70 return result.body.then(function (body) { 71 result.body = body; 72 return result; 73 }); 74 } 75 return result; 76 }).then(tr.normalize).nodeify(cb); 77} 78 79function Transformer(tr) { 80 assert(tr, 'Transformer must be an object'); 81 assert(typeof tr.name === 'string', 'Transformer must have a name'); 82 assert(typeof tr.outputFormat === 'string', 'Transformer must have an output format'); 83 assert([ 84 'compile', 85 'compileAsync', 86 'compileFile', 87 'compileFileAsync', 88 'compileClient', 89 'compileClientAsync', 90 'compileFileClient', 91 'compileFileClientAsync', 92 'render', 93 'renderAsync', 94 'renderFile', 95 'renderFileAsync' 96 ].some(function (method) { 97 return typeof tr[method] === 'function'; 98 }), 'Transformer must implement at least one of the potential methods.'); 99 this._tr = tr; 100 this.name = this._tr.name; 101 this.outputFormat = this._tr.outputFormat; 102 this.inputFormats = this._tr.inputFormats || [this.name]; 103} 104 105var fallbacks = { 106 compile: ['compile', 'render'], 107 compileAsync: ['compileAsync', 'compile', 'render'], 108 compileFile: ['compileFile', 'compile', 'renderFile', 'render'], 109 compileFileAsync: [ 110 'compileFileAsync', 'compileFile', 'compileAsync', 'compile', 111 'renderFile', 'render' 112 ], 113 compileClient: ['compileClient'], 114 compileClientAsync: ['compileClientAsync', 'compileClient'], 115 compileFileClient: ['compileFileClient', 'compileClient'], 116 compileFileClientAsync: [ 117 'compileFileClientAsync', 'compileFileClient', 'compileClientAsync', 'compileClient' 118 ], 119 render: ['render', 'compile'], 120 renderAsync: ['renderAsync', 'render', 'compileAsync', 'compile'], 121 renderFile: ['renderFile', 'render', 'compileFile', 'compile'], 122 renderFileAsync: [ 123 'renderFileAsync', 'renderFile', 'renderAsync', 'render', 124 'compileFileAsync', 'compileFile', 'compileAsync', 'compile' 125 ] 126}; 127 128Transformer.prototype._hasMethod = function (method) { 129 return typeof this._tr[method] === 'function'; 130}; 131Transformer.prototype.can = function (method) { 132 return fallbacks[method].some(function (method) { 133 return this._hasMethod(method); 134 }.bind(this)); 135}; 136 137/* COMPILE */ 138 139Transformer.prototype.compile = function (str, options) { 140 if (!this._hasMethod('compile')) { 141 if (this.can('render')) { 142 var _this = this; 143 return { 144 fn: function (locals) { 145 return tr.normalize(_this._tr.render(str, options, locals)).body; 146 }, 147 dependencies: [] 148 }; 149 } 150 if (this.can('compileAsync')) { 151 throw new Error('The Transform "' + this.name + '" does not support synchronous compilation'); 152 } else if (this.can('compileFileAsync')) { 153 throw new Error('The Transform "' + this.name + '" does not support compiling plain strings'); 154 } else { 155 throw new Error('The Transform "' + this.name + '" does not support compilation'); 156 } 157 } 158 return tr.normalizeFn(this._tr.compile(str, options)); 159}; 160Transformer.prototype.compileAsync = function (str, options, cb) { 161 if (!this.can('compileAsync')) { // compileFile* || renderFile* || renderAsync || compile*Client* 162 return Promise.reject(new Error('The Transform "' + this.name + '" does not support compiling plain strings')).nodeify(cb); 163 } 164 if (this._hasMethod('compileAsync')) { 165 return tr.normalizeFnAsync(this._tr.compileAsync(str, options), cb); 166 } else { // render || compile 167 return tr.normalizeFnAsync(this.compile(str, options), cb); 168 } 169}; 170Transformer.prototype.compileFile = function (filename, options) { 171 if (!this.can('compileFile')) { // compile*Client* || compile*Async || render*Async 172 throw new Error('The Transform "' + this.name + '" does not support synchronous compilation'); 173 } 174 if (this._hasMethod('compileFile')) { 175 return tr.normalizeFn(this._tr.compileFile(filename, options)); 176 } else if (this._hasMethod('renderFile')) { 177 return tr.normalizeFn(function (locals) { 178 return tr.normalize(this._tr.renderFile(filename, options, locals)).body; 179 }.bind(this)); 180 } else { // render || compile 181 if (!options) options = {}; 182 if (options.filename === undefined) options.filename = filename; 183 return this.compile(tr.readFileSync(filename, 'utf8'), options); 184 } 185}; 186Transformer.prototype.compileFileAsync = function (filename, options, cb) { 187 if (!this.can('compileFileAsync')) { 188 return Promise.reject(new Error('The Transform "' + this.name + '" does not support compilation')); 189 } 190 if (this._hasMethod('compileFileAsync')) { 191 return tr.normalizeFnAsync(this._tr.compileFileAsync(filename, options), cb); 192 } else if (this._hasMethod('compileFile') || this._hasMethod('renderFile')) { 193 return tr.normalizeFnAsync(this.compileFile(filename, options), cb); 194 } else { // compileAsync || compile || render 195 if (!options) options = {}; 196 if (options.filename === undefined) options.filename = filename; 197 return tr.normalizeFnAsync(tr.readFile(filename, 'utf8').then(function (str) { 198 if (this._hasMethod('compileAsync')) { 199 return this._tr.compileAsync(str, options); 200 } else { // compile || render 201 return this.compile(str, options); 202 } 203 }.bind(this)), cb); 204 } 205}; 206 207/* COMPILE CLIENT */ 208 209 210Transformer.prototype.compileClient = function (str, options) { 211 if (!this.can('compileClient')) { 212 if (this.can('compileClientAsync')) { 213 throw new Error('The Transform "' + this.name + '" does not support compiling for the client synchronously.'); 214 } else if (this.can('compileFileClientAsync')) { 215 throw new Error('The Transform "' + this.name + '" does not support compiling for the client from a string.'); 216 } else { 217 throw new Error('The Transform "' + this.name + '" does not support compiling for the client'); 218 } 219 } 220 return tr.normalize(this._tr.compileClient(str, options)); 221}; 222Transformer.prototype.compileClientAsync = function (str, options, cb) { 223 if (!this.can('compileClientAsync')) { 224 if (this.can('compileFileClientAsync')) { 225 return Promise.reject(new Error('The Transform "' + this.name + '" does not support compiling for the client from a string.')).nodeify(cb); 226 } else { 227 return Promise.reject(new Error('The Transform "' + this.name + '" does not support compiling for the client')).nodeify(cb); 228 } 229 } 230 if (this._hasMethod('compileClientAsync')) { 231 return tr.normalizeAsync(this._tr.compileClientAsync(str, options), cb); 232 } else { 233 return tr.normalizeAsync(this._tr.compileClient(str, options), cb); 234 } 235}; 236Transformer.prototype.compileFileClient = function (filename, options) { 237 if (!this.can('compileFileClient')) { 238 if (this.can('compileFileClientAsync')) { 239 throw new Error('The Transform "' + this.name + '" does not support compiling for the client synchronously.'); 240 } else { 241 throw new Error('The Transform "' + this.name + '" does not support compiling for the client'); 242 } 243 } 244 if (this._hasMethod('compileFileClient')) { 245 return tr.normalize(this._tr.compileFileClient(filename, options)); 246 } else { 247 if (!options) options = {}; 248 if (options.filename === undefined) options.filename = filename; 249 return tr.normalize(this._tr.compileClient(tr.readFileSync(filename, 'utf8'), options)); 250 } 251}; 252Transformer.prototype.compileFileClientAsync = function (filename, options, cb) { 253 if (!this.can('compileFileClientAsync')) { 254 return Promise.reject(new Error('The Transform "' + this.name + '" does not support compiling for the client')).nodeify(cb) 255 } 256 if (this._hasMethod('compileFileClientAsync')) { 257 return tr.normalizeAsync(this._tr.compileFileClientAsync(filename, options), cb); 258 } else if (this._hasMethod('compileFileClient')) { 259 return tr.normalizeAsync(this._tr.compileFileClient(filename, options), cb); 260 } else { 261 if (!options) options = {}; 262 if (options.filename === undefined) options.filename = filename; 263 return tr.normalizeAsync(tr.readFile(filename, 'utf8').then(function (str) { 264 if (this._hasMethod('compileClientAsync')) { 265 return this._tr.compileClientAsync(str, options); 266 } else { 267 return this._tr.compileClient(str, options); 268 } 269 }.bind(this)), cb); 270 } 271}; 272 273/* RENDER */ 274 275Transformer.prototype.render = function (str, options, locals) { 276 if (!this.can('render')) { 277 if (this.can('renderAsync')) { 278 throw new Error('The Transform "' + this.name + '" does not support rendering synchronously.'); 279 } else if (this.can('renderFileAsync')) { 280 throw new Error('The Transform "' + this.name + '" does not support rendering from a string.'); 281 } else { 282 throw new Error('The Transform "' + this.name + '" does not support rendering'); 283 } 284 } 285 if (this._hasMethod('render')) { 286 return tr.normalize(this._tr.render(str, options, locals)); 287 } else { 288 var compiled = tr.normalizeFn(this._tr.compile(str, options)); 289 var body = compiled.fn(locals || options); 290 if (typeof body !== 'string') { 291 throw new Error('The Transform "' + this.name + '" does not support rendering synchronously.'); 292 } 293 return tr.normalize({body: body, dependencies: compiled.dependencies}); 294 } 295}; 296Transformer.prototype.renderAsync = function (str, options, locals, cb) { 297 if (typeof locals === 'function') { 298 cb = locals; 299 locals = options; 300 } 301 if (!this.can('renderAsync')) { 302 if (this.can('renderFileAsync')) { 303 return Promise.reject(new Error('The Transform "' + this.name + '" does not support rendering from a string.')).nodeify(cb); 304 } else { 305 return Promise.reject(new Error('The Transform "' + this.name + '" does not support rendering')).nodeify(cb); 306 } 307 } 308 if (this._hasMethod('renderAsync')) { 309 return tr.normalizeAsync(this._tr.renderAsync(str, options, locals), cb); 310 } else if (this._hasMethod('render')) { 311 return tr.normalizeAsync(this._tr.render(str, options, locals), cb); 312 } else { 313 return tr.normalizeAsync(this.compileAsync(str, options).then(function (compiled) { 314 return {body: compiled.fn(locals || options), dependencies: compiled.dependencies}; 315 }), cb); 316 } 317}; 318Transformer.prototype.renderFile = function (filename, options, locals) { 319 if (!this.can('renderFile')) { // *Async, *Client 320 throw new Error('The Transform "' + this.name + '" does not support rendering synchronously.'); 321 } 322 323 if (this._hasMethod('renderFile')) { 324 return tr.normalize(this._tr.renderFile(filename, options, locals)); 325 } else if (this._hasMethod('render')) { 326 if (!options) options = {}; 327 if (options.filename === undefined) options.filename = filename; 328 return tr.normalize(this._tr.render(tr.readFileSync(filename, 'utf8'), options, locals)); 329 } else { // compile || compileFile 330 var compiled = this.compileFile(filename, options); 331 return tr.normalize({body: compiled.fn(locals || options), dependencies: compiled.dependencies}); 332 } 333}; 334Transformer.prototype.renderFileAsync = function (filename, options, locals, cb) { 335 if (!this.can('renderFileAsync')) { // *Client 336 throw new Error('The Transform "' + this.name + '" does not support rendering.'); 337 } 338 339 if (typeof locals === 'function') { 340 cb = locals; 341 locals = options; 342 } 343 if (this._hasMethod('renderFileAsync')) { 344 return tr.normalizeAsync(this._tr.renderFileAsync(filename, options, locals), cb); 345 } else if (this._hasMethod('renderFile')) { 346 return tr.normalizeAsync(this._tr.renderFile(filename, options, locals), cb); 347 } else if (this._hasMethod('compile') || this._hasMethod('compileAsync') 348 || this._hasMethod('compileFile') || this._hasMethod('compileFileAsync')) { 349 return tr.normalizeAsync(this.compileFileAsync(filename, options).then(function (compiled) { 350 return {body: compiled.fn(locals || options), dependencies: compiled.dependencies}; 351 }), cb); 352 } else { // render || renderAsync 353 if (!options) options = {}; 354 if (options.filename === undefined) options.filename = filename; 355 return tr.normalizeAsync(tr.readFile(filename, 'utf8').then(function (str) { 356 return this.renderAsync(str, options, locals); 357 }.bind(this)), cb); 358 } 359}; 360