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.
494 lines
13 KiB
494 lines
13 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] = 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); |
|
};
|
|
|