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