index.js 14 KB

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