You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

465 lines
12 KiB

'use strict';
/*!
* Pug
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs');
var path = require('path');
var lex = require('pug-lexer');
var stripComments = require('pug-strip-comments');
var parse = require('pug-parser');
var load = require('pug-load');
var filters = require('pug-filters');
var link = require('pug-linker');
var generateCode = require('pug-code-gen');
var runtime = require('pug-runtime');
var runtimeWrap = require('pug-runtime/wrap');
/**
* Name for detection
*/
exports.name = 'Pug';
/**
* Pug runtime helpers.
*/
exports.runtime = runtime;
/**
* Template function cache.
*/
exports.cache = {};
function applyPlugins(value, options, plugins, name) {
return plugins.reduce(function (value, plugin) {
return (
plugin[name]
? plugin[name](value, options)
: value
);
}, value);
}
function findReplacementFunc(plugins, name) {
var eligiblePlugins = plugins.filter(function (plugin) {
return plugin[name];
});
if (eligiblePlugins.length > 1) {
throw new Error('Two or more plugins all implement ' + name + ' method.');
} else if (eligiblePlugins.length) {
return eligiblePlugins[0][name].bind(eligiblePlugins[0]);
}
return null;
}
/**
* Object for global custom filters. Note that you can also just pass a `filters`
* option to any other method.
*/
exports.filters = {};
/**
* Compile the given `str` of pug and return a function body.
*
* @param {String} str
* @param {Object} options
* @return {Object}
* @api private
*/
function compileBody(str, options){
var debug_sources = {};
debug_sources[options.filename] = str;
var dependencies = [];
var plugins = options.plugins || [];
var ast = load.string(str, {
filename: options.filename,
basedir: options.basedir,
lex: function (str, options) {
var lexOptions = {};
Object.keys(options).forEach(function (key) {
lexOptions[key] = options[key];
});
lexOptions.plugins = plugins.filter(function (plugin) {
return !!plugin.lex;
}).map(function (plugin) {
return plugin.lex;
});
var contents = applyPlugins(str, {filename: options.filename}, plugins, 'preLex');
return applyPlugins(lex(contents, lexOptions), options, plugins, 'postLex');
},
parse: function (tokens, options) {
tokens = tokens.map(function (token) {
if (token.type === 'path' && path.extname(token.val) === '') {
return {
type: 'path',
loc: token.loc,
val: token.val + '.pug'
};
}
return token;
});
tokens = stripComments(tokens, options);
tokens = applyPlugins(tokens, options, plugins, 'preParse');
var parseOptions = {};
Object.keys(options).forEach(function (key) {
parseOptions[key] = options[key];
});
parseOptions.plugins = plugins.filter(function (plugin) {
return !!plugin.parse;
}).map(function (plugin) {
return plugin.parse;
});
return applyPlugins(
applyPlugins(parse(tokens, parseOptions), options, plugins, 'postParse'),
options, plugins, 'preLoad'
);
},
resolve: function (filename, source, loadOptions) {
var replacementFunc = findReplacementFunc(plugins, 'resolve');
if (replacementFunc) {
return replacementFunc(filename, source, options);
}
return load.resolve(filename, source, loadOptions);
},
read: function (filename, loadOptions) {
dependencies.push(filename);
var contents;
var replacementFunc = findReplacementFunc(plugins, 'read');
if (replacementFunc) {
contents = replacementFunc(filename, options);
} else {
contents = load.read(filename, loadOptions);
}
debug_sources[filename] = contents;
return contents;
}
});
ast = applyPlugins(ast, options, plugins, 'postLoad');
ast = applyPlugins(ast, options, plugins, 'preFilters');
var filtersSet = {};
Object.keys(exports.filters).forEach(function (key) {
filtersSet[key] = exports.filters[key];
});
if (options.filters) {
Object.keys(options.filters).forEach(function (key) {
filtersSet[key] = options.filters[key];
});
}
ast = filters.handleFilters(ast, filtersSet, options.filterOptions, options.filterAliases);
ast = applyPlugins(ast, options, plugins, 'postFilters');
ast = applyPlugins(ast, options, plugins, 'preLink');
ast = link(ast);
ast = applyPlugins(ast, options, plugins, 'postLink');
// Compile
ast = applyPlugins(ast, options, plugins, 'preCodeGen');
var js = generateCode(ast, {
pretty: options.pretty,
compileDebug: options.compileDebug,
doctype: options.doctype,
inlineRuntimeFunctions: options.inlineRuntimeFunctions,
globals: options.globals,
self: options.self,
includeSources: options.includeSources ? debug_sources : false,
templateName: options.templateName
});
js = applyPlugins(js, options, plugins, 'postCodeGen');
// Debug compiler
if (options.debug) {
console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, ' '));
}
return {body: js, dependencies: dependencies};
}
/**
* Get the template from a string or a file, either compiled on-the-fly or
* read from cache (if enabled), and cache the template if needed.
*
* If `str` is not set, the file specified in `options.filename` will be read.
*
* If `options.cache` is true, this function reads the file from
* `options.filename` so it must be set prior to calling this function.
*
* @param {Object} options
* @param {String=} str
* @return {Function}
* @api private
*/
function handleTemplateCache (options, str) {
var key = options.filename;
if (options.cache && exports.cache[key]) {
return exports.cache[key];
} else {
if (str === undefined) str = fs.readFileSync(options.filename, 'utf8');
var templ = exports.compile(str, options);
if (options.cache) exports.cache[key] = templ;
return templ;
}
}
/**
* Compile a `Function` representation of the given pug `str`.
*
* Options:
*
* - `compileDebug` when `false` debugging code is stripped from the compiled
template, when it is explicitly `true`, the source code is included in
the compiled template for better accuracy.
* - `filename` used to improve errors when `compileDebug` is not `false` and to resolve imports/extends
*
* @param {String} str
* @param {Options} options
* @return {Function}
* @api public
*/
exports.compile = function(str, options){
var options = options || {}
str = String(str);
var parsed = compileBody(str, {
compileDebug: options.compileDebug !== false,
filename: options.filename,
basedir: options.basedir,
pretty: options.pretty,
doctype: options.doctype,
inlineRuntimeFunctions: options.inlineRuntimeFunctions,
globals: options.globals,
self: options.self,
includeSources: options.compileDebug === true,
debug: options.debug,
templateName: 'template',
filters: options.filters,
filterOptions: options.filterOptions,
filterAliases: options.filterAliases,
plugins: options.plugins,
});
var res = options.inlineRuntimeFunctions
? new Function('', parsed.body + ';return template;')()
: runtimeWrap(parsed.body);
res.dependencies = parsed.dependencies;
return res;
};
/**
* Compile a JavaScript source representation of the given pug `str`.
*
* Options:
*
* - `compileDebug` When it is `true`, the source code is included in
* the compiled template for better error messages.
* - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
* - `name` the name of the resulting function (defaults to "template")
* - `module` when it is explicitly `true`, the source code include export module syntax
*
* @param {String} str
* @param {Options} options
* @return {Object}
* @api public
*/
exports.compileClientWithDependenciesTracked = function(str, options){
var options = options || {};
str = String(str);
var parsed = compileBody(str, {
compileDebug: options.compileDebug,
filename: options.filename,
basedir: options.basedir,
pretty: options.pretty,
doctype: options.doctype,
inlineRuntimeFunctions: options.inlineRuntimeFunctions !== false,
globals: options.globals,
self: options.self,
includeSources: options.compileDebug,
debug: options.debug,
templateName: options.name || 'template',
filters: options.filters,
filterOptions: options.filterOptions,
filterAliases: options.filterAliases,
plugins: options.plugins
});
var body = parsed.body;
if(options.module) {
if(options.inlineRuntimeFunctions === false) {
body = 'var pug = require("pug-runtime");' + body;
}
body += ' module.exports = ' + (options.name || 'template') + ';';
}
return {body: body, dependencies: parsed.dependencies};
};
/**
* Compile a JavaScript source representation of the given pug `str`.
*
* Options:
*
* - `compileDebug` When it is `true`, the source code is included in
* the compiled template for better error messages.
* - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
* - `name` the name of the resulting function (defaults to "template")
*
* @param {String} str
* @param {Options} options
* @return {String}
* @api public
*/
exports.compileClient = function (str, options) {
return exports.compileClientWithDependenciesTracked(str, options).body;
};
/**
* Compile a `Function` representation of the given pug file.
*
* Options:
*
* - `compileDebug` when `false` debugging code is stripped from the compiled
template, when it is explicitly `true`, the source code is included in
the compiled template for better accuracy.
*
* @param {String} path
* @param {Options} options
* @return {Function}
* @api public
*/
exports.compileFile = function (path, options) {
options = options || {};
options.filename = path;
return handleTemplateCache(options);
};
/**
* Render the given `str` of pug.
*
* Options:
*
* - `cache` enable template caching
* - `filename` filename required for `include` / `extends` and caching
*
* @param {String} str
* @param {Object|Function} options or fn
* @param {Function|undefined} fn
* @returns {String}
* @api public
*/
exports.render = function(str, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res;
try {
res = exports.render(str, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
// cache requires .filename
if (options.cache && !options.filename) {
throw new Error('the "filename" option is required for caching');
}
return handleTemplateCache(options, str)(options);
};
/**
* Render a Pug file at the given `path`.
*
* @param {String} path
* @param {Object|Function} options or callback
* @param {Function|undefined} fn
* @returns {String}
* @api public
*/
exports.renderFile = function(path, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res;
try {
res = exports.renderFile(path, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
options.filename = path;
return handleTemplateCache(options)(options);
};
/**
* Compile a Pug file at the given `path` for use on the client.
*
* @param {String} path
* @param {Object} options
* @returns {String}
* @api public
*/
exports.compileFileClient = function(path, options){
var key = path + ':client';
options = options || {};
options.filename = path;
if (options.cache && exports.cache[key]) {
return exports.cache[key];
}
var str = fs.readFileSync(options.filename, 'utf8');
var out = exports.compileClient(str, options);
if (options.cache) exports.cache[key] = out;
return out;
};
/**
* Express support.
*/
exports.__express = function(path, options, fn) {
if(options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
options.compileDebug = false;
}
exports.renderFile(path, options, fn);
}