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