index.js 13 KB

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