494 lines
13 KiB
JavaScript
494 lines
13 KiB
JavaScript
'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] = Buffer.isBuffer(contents)
|
|
? contents.toString('utf8')
|
|
: 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 = (findReplacementFunc(plugins, 'generateCode') || 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);
|
|
};
|