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