index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. 'use strict';
  2. /*!
  3. * Pug
  4. * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
  5. * MIT Licensed
  6. */
  7. /**
  8. * Module dependencies.
  9. */
  10. var fs = require('fs');
  11. var path = require('path');
  12. var lex = require('pug-lexer');
  13. var stripComments = require('pug-strip-comments');
  14. var parse = require('pug-parser');
  15. var load = require('pug-load');
  16. var filters = require('pug-filters');
  17. var link = require('pug-linker');
  18. var generateCode = require('pug-code-gen');
  19. var runtime = require('pug-runtime');
  20. var runtimeWrap = require('pug-runtime/wrap');
  21. /**
  22. * Name for detection
  23. */
  24. exports.name = 'Pug';
  25. /**
  26. * Pug runtime helpers.
  27. */
  28. exports.runtime = runtime;
  29. /**
  30. * Template function cache.
  31. */
  32. exports.cache = {};
  33. function applyPlugins(value, options, plugins, name) {
  34. return plugins.reduce(function(value, plugin) {
  35. return plugin[name] ? plugin[name](value, options) : value;
  36. }, value);
  37. }
  38. function findReplacementFunc(plugins, name) {
  39. var eligiblePlugins = plugins.filter(function(plugin) {
  40. return plugin[name];
  41. });
  42. if (eligiblePlugins.length > 1) {
  43. throw new Error('Two or more plugins all implement ' + name + ' method.');
  44. } else if (eligiblePlugins.length) {
  45. return eligiblePlugins[0][name].bind(eligiblePlugins[0]);
  46. }
  47. return null;
  48. }
  49. /**
  50. * Object for global custom filters. Note that you can also just pass a `filters`
  51. * option to any other method.
  52. */
  53. exports.filters = {};
  54. /**
  55. * Compile the given `str` of pug and return a function body.
  56. *
  57. * @param {String} str
  58. * @param {Object} options
  59. * @return {Object}
  60. * @api private
  61. */
  62. function compileBody(str, options) {
  63. var debug_sources = {};
  64. debug_sources[options.filename] = str;
  65. var dependencies = [];
  66. var plugins = options.plugins || [];
  67. var ast = load.string(str, {
  68. filename: options.filename,
  69. basedir: options.basedir,
  70. lex: function(str, options) {
  71. var lexOptions = {};
  72. Object.keys(options).forEach(function(key) {
  73. lexOptions[key] = options[key];
  74. });
  75. lexOptions.plugins = plugins
  76. .filter(function(plugin) {
  77. return !!plugin.lex;
  78. })
  79. .map(function(plugin) {
  80. return plugin.lex;
  81. });
  82. var contents = applyPlugins(
  83. str,
  84. {filename: options.filename},
  85. plugins,
  86. 'preLex'
  87. );
  88. return applyPlugins(
  89. lex(contents, lexOptions),
  90. options,
  91. plugins,
  92. 'postLex'
  93. );
  94. },
  95. parse: function(tokens, options) {
  96. tokens = tokens.map(function(token) {
  97. if (token.type === 'path' && path.extname(token.val) === '') {
  98. return {
  99. type: 'path',
  100. loc: token.loc,
  101. val: token.val + '.pug',
  102. };
  103. }
  104. return token;
  105. });
  106. tokens = stripComments(tokens, options);
  107. tokens = applyPlugins(tokens, options, plugins, 'preParse');
  108. var parseOptions = {};
  109. Object.keys(options).forEach(function(key) {
  110. parseOptions[key] = options[key];
  111. });
  112. parseOptions.plugins = plugins
  113. .filter(function(plugin) {
  114. return !!plugin.parse;
  115. })
  116. .map(function(plugin) {
  117. return plugin.parse;
  118. });
  119. return applyPlugins(
  120. applyPlugins(
  121. parse(tokens, parseOptions),
  122. options,
  123. plugins,
  124. 'postParse'
  125. ),
  126. options,
  127. plugins,
  128. 'preLoad'
  129. );
  130. },
  131. resolve: function(filename, source, loadOptions) {
  132. var replacementFunc = findReplacementFunc(plugins, 'resolve');
  133. if (replacementFunc) {
  134. return replacementFunc(filename, source, options);
  135. }
  136. return load.resolve(filename, source, loadOptions);
  137. },
  138. read: function(filename, loadOptions) {
  139. dependencies.push(filename);
  140. var contents;
  141. var replacementFunc = findReplacementFunc(plugins, 'read');
  142. if (replacementFunc) {
  143. contents = replacementFunc(filename, options);
  144. } else {
  145. contents = load.read(filename, loadOptions);
  146. }
  147. debug_sources[filename] = Buffer.isBuffer(contents)
  148. ? contents.toString('utf8')
  149. : contents;
  150. return contents;
  151. },
  152. });
  153. ast = applyPlugins(ast, options, plugins, 'postLoad');
  154. ast = applyPlugins(ast, options, plugins, 'preFilters');
  155. var filtersSet = {};
  156. Object.keys(exports.filters).forEach(function(key) {
  157. filtersSet[key] = exports.filters[key];
  158. });
  159. if (options.filters) {
  160. Object.keys(options.filters).forEach(function(key) {
  161. filtersSet[key] = options.filters[key];
  162. });
  163. }
  164. ast = filters.handleFilters(
  165. ast,
  166. filtersSet,
  167. options.filterOptions,
  168. options.filterAliases
  169. );
  170. ast = applyPlugins(ast, options, plugins, 'postFilters');
  171. ast = applyPlugins(ast, options, plugins, 'preLink');
  172. ast = link(ast);
  173. ast = applyPlugins(ast, options, plugins, 'postLink');
  174. // Compile
  175. ast = applyPlugins(ast, options, plugins, 'preCodeGen');
  176. var js = (findReplacementFunc(plugins, 'generateCode') || generateCode)(ast, {
  177. pretty: options.pretty,
  178. compileDebug: options.compileDebug,
  179. doctype: options.doctype,
  180. inlineRuntimeFunctions: options.inlineRuntimeFunctions,
  181. globals: options.globals,
  182. self: options.self,
  183. includeSources: options.includeSources ? debug_sources : false,
  184. templateName: options.templateName,
  185. });
  186. js = applyPlugins(js, options, plugins, 'postCodeGen');
  187. // Debug compiler
  188. if (options.debug) {
  189. console.error(
  190. '\nCompiled Function:\n\n\u001b[90m%s\u001b[0m',
  191. js.replace(/^/gm, ' ')
  192. );
  193. }
  194. return {body: js, dependencies: dependencies};
  195. }
  196. /**
  197. * Get the template from a string or a file, either compiled on-the-fly or
  198. * read from cache (if enabled), and cache the template if needed.
  199. *
  200. * If `str` is not set, the file specified in `options.filename` will be read.
  201. *
  202. * If `options.cache` is true, this function reads the file from
  203. * `options.filename` so it must be set prior to calling this function.
  204. *
  205. * @param {Object} options
  206. * @param {String=} str
  207. * @return {Function}
  208. * @api private
  209. */
  210. function handleTemplateCache(options, str) {
  211. var key = options.filename;
  212. if (options.cache && exports.cache[key]) {
  213. return exports.cache[key];
  214. } else {
  215. if (str === undefined) str = fs.readFileSync(options.filename, 'utf8');
  216. var templ = exports.compile(str, options);
  217. if (options.cache) exports.cache[key] = templ;
  218. return templ;
  219. }
  220. }
  221. /**
  222. * Compile a `Function` representation of the given pug `str`.
  223. *
  224. * Options:
  225. *
  226. * - `compileDebug` when `false` debugging code is stripped from the compiled
  227. template, when it is explicitly `true`, the source code is included in
  228. the compiled template for better accuracy.
  229. * - `filename` used to improve errors when `compileDebug` is not `false` and to resolve imports/extends
  230. *
  231. * @param {String} str
  232. * @param {Options} options
  233. * @return {Function}
  234. * @api public
  235. */
  236. exports.compile = function(str, options) {
  237. var options = options || {};
  238. str = String(str);
  239. var parsed = compileBody(str, {
  240. compileDebug: options.compileDebug !== false,
  241. filename: options.filename,
  242. basedir: options.basedir,
  243. pretty: options.pretty,
  244. doctype: options.doctype,
  245. inlineRuntimeFunctions: options.inlineRuntimeFunctions,
  246. globals: options.globals,
  247. self: options.self,
  248. includeSources: options.compileDebug === true,
  249. debug: options.debug,
  250. templateName: 'template',
  251. filters: options.filters,
  252. filterOptions: options.filterOptions,
  253. filterAliases: options.filterAliases,
  254. plugins: options.plugins,
  255. });
  256. var res = options.inlineRuntimeFunctions
  257. ? new Function('', parsed.body + ';return template;')()
  258. : runtimeWrap(parsed.body);
  259. res.dependencies = parsed.dependencies;
  260. return res;
  261. };
  262. /**
  263. * Compile a JavaScript source representation of the given pug `str`.
  264. *
  265. * Options:
  266. *
  267. * - `compileDebug` When it is `true`, the source code is included in
  268. * the compiled template for better error messages.
  269. * - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
  270. * - `name` the name of the resulting function (defaults to "template")
  271. * - `module` when it is explicitly `true`, the source code include export module syntax
  272. *
  273. * @param {String} str
  274. * @param {Options} options
  275. * @return {Object}
  276. * @api public
  277. */
  278. exports.compileClientWithDependenciesTracked = function(str, options) {
  279. var options = options || {};
  280. str = String(str);
  281. var parsed = compileBody(str, {
  282. compileDebug: options.compileDebug,
  283. filename: options.filename,
  284. basedir: options.basedir,
  285. pretty: options.pretty,
  286. doctype: options.doctype,
  287. inlineRuntimeFunctions: options.inlineRuntimeFunctions !== false,
  288. globals: options.globals,
  289. self: options.self,
  290. includeSources: options.compileDebug,
  291. debug: options.debug,
  292. templateName: options.name || 'template',
  293. filters: options.filters,
  294. filterOptions: options.filterOptions,
  295. filterAliases: options.filterAliases,
  296. plugins: options.plugins,
  297. });
  298. var body = parsed.body;
  299. if (options.module) {
  300. if (options.inlineRuntimeFunctions === false) {
  301. body = 'var pug = require("pug-runtime");' + body;
  302. }
  303. body += ' module.exports = ' + (options.name || 'template') + ';';
  304. }
  305. return {body: body, dependencies: parsed.dependencies};
  306. };
  307. /**
  308. * Compile a JavaScript source representation of the given pug `str`.
  309. *
  310. * Options:
  311. *
  312. * - `compileDebug` When it is `true`, the source code is included in
  313. * the compiled template for better error messages.
  314. * - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
  315. * - `name` the name of the resulting function (defaults to "template")
  316. *
  317. * @param {String} str
  318. * @param {Options} options
  319. * @return {String}
  320. * @api public
  321. */
  322. exports.compileClient = function(str, options) {
  323. return exports.compileClientWithDependenciesTracked(str, options).body;
  324. };
  325. /**
  326. * Compile a `Function` representation of the given pug file.
  327. *
  328. * Options:
  329. *
  330. * - `compileDebug` when `false` debugging code is stripped from the compiled
  331. template, when it is explicitly `true`, the source code is included in
  332. the compiled template for better accuracy.
  333. *
  334. * @param {String} path
  335. * @param {Options} options
  336. * @return {Function}
  337. * @api public
  338. */
  339. exports.compileFile = function(path, options) {
  340. options = options || {};
  341. options.filename = path;
  342. return handleTemplateCache(options);
  343. };
  344. /**
  345. * Render the given `str` of pug.
  346. *
  347. * Options:
  348. *
  349. * - `cache` enable template caching
  350. * - `filename` filename required for `include` / `extends` and caching
  351. *
  352. * @param {String} str
  353. * @param {Object|Function} options or fn
  354. * @param {Function|undefined} fn
  355. * @returns {String}
  356. * @api public
  357. */
  358. exports.render = function(str, options, fn) {
  359. // support callback API
  360. if ('function' == typeof options) {
  361. (fn = options), (options = undefined);
  362. }
  363. if (typeof fn === 'function') {
  364. var res;
  365. try {
  366. res = exports.render(str, options);
  367. } catch (ex) {
  368. return fn(ex);
  369. }
  370. return fn(null, res);
  371. }
  372. options = options || {};
  373. // cache requires .filename
  374. if (options.cache && !options.filename) {
  375. throw new Error('the "filename" option is required for caching');
  376. }
  377. return handleTemplateCache(options, str)(options);
  378. };
  379. /**
  380. * Render a Pug file at the given `path`.
  381. *
  382. * @param {String} path
  383. * @param {Object|Function} options or callback
  384. * @param {Function|undefined} fn
  385. * @returns {String}
  386. * @api public
  387. */
  388. exports.renderFile = function(path, options, fn) {
  389. // support callback API
  390. if ('function' == typeof options) {
  391. (fn = options), (options = undefined);
  392. }
  393. if (typeof fn === 'function') {
  394. var res;
  395. try {
  396. res = exports.renderFile(path, options);
  397. } catch (ex) {
  398. return fn(ex);
  399. }
  400. return fn(null, res);
  401. }
  402. options = options || {};
  403. options.filename = path;
  404. return handleTemplateCache(options)(options);
  405. };
  406. /**
  407. * Compile a Pug file at the given `path` for use on the client.
  408. *
  409. * @param {String} path
  410. * @param {Object} options
  411. * @returns {String}
  412. * @api public
  413. */
  414. exports.compileFileClient = function(path, options) {
  415. var key = path + ':client';
  416. options = options || {};
  417. options.filename = path;
  418. if (options.cache && exports.cache[key]) {
  419. return exports.cache[key];
  420. }
  421. var str = fs.readFileSync(options.filename, 'utf8');
  422. var out = exports.compileClient(str, options);
  423. if (options.cache) exports.cache[key] = out;
  424. return out;
  425. };
  426. /**
  427. * Express support.
  428. */
  429. exports.__express = function(path, options, fn) {
  430. if (
  431. options.compileDebug == undefined &&
  432. process.env.NODE_ENV === 'production'
  433. ) {
  434. options.compileDebug = false;
  435. }
  436. exports.renderFile(path, options, fn);
  437. };