1/* 2 * EJS Embedded JavaScript templates 3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org) 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17*/ 18 19'use strict'; 20 21/** 22 * @file Embedded JavaScript templating engine. {@link http://ejs.co} 23 * @author Matthew Eernisse <mde@fleegix.org> 24 * @author Tiancheng "Timothy" Gu <timothygu99@gmail.com> 25 * @project EJS 26 * @license {@link http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0} 27 */ 28 29/** 30 * EJS internal functions. 31 * 32 * Technically this "module" lies in the same file as {@link module:ejs}, for 33 * the sake of organization all the private functions re grouped into this 34 * module. 35 * 36 * @module ejs-internal 37 * @private 38 */ 39 40/** 41 * Embedded JavaScript templating engine. 42 * 43 * @module ejs 44 * @public 45 */ 46 47 48var fs = require('fs'); 49var path = require('path'); 50var utils = require('./utils'); 51 52var scopeOptionWarned = false; 53/** @type {string} */ 54var _VERSION_STRING = require('../package.json').version; 55var _DEFAULT_OPEN_DELIMITER = '<'; 56var _DEFAULT_CLOSE_DELIMITER = '>'; 57var _DEFAULT_DELIMITER = '%'; 58var _DEFAULT_LOCALS_NAME = 'locals'; 59var _NAME = 'ejs'; 60var _REGEX_STRING = '(<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)'; 61var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug', 62 'client', '_with', 'rmWhitespace', 'strict', 'filename', 'async']; 63// We don't allow 'cache' option to be passed in the data obj for 64// the normal `render` call, but this is where Express 2 & 3 put it 65// so we make an exception for `renderFile` 66var _OPTS_PASSABLE_WITH_DATA_EXPRESS = _OPTS_PASSABLE_WITH_DATA.concat('cache'); 67var _BOM = /^\uFEFF/; 68var _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; 69 70/** 71 * EJS template function cache. This can be a LRU object from lru-cache NPM 72 * module. By default, it is {@link module:utils.cache}, a simple in-process 73 * cache that grows continuously. 74 * 75 * @type {Cache} 76 */ 77 78exports.cache = utils.cache; 79 80/** 81 * Custom file loader. Useful for template preprocessing or restricting access 82 * to a certain part of the filesystem. 83 * 84 * @type {fileLoader} 85 */ 86 87exports.fileLoader = fs.readFileSync; 88 89/** 90 * Name of the object containing the locals. 91 * 92 * This variable is overridden by {@link Options}`.localsName` if it is not 93 * `undefined`. 94 * 95 * @type {String} 96 * @public 97 */ 98 99exports.localsName = _DEFAULT_LOCALS_NAME; 100 101/** 102 * Promise implementation -- defaults to the native implementation if available 103 * This is mostly just for testability 104 * 105 * @type {PromiseConstructorLike} 106 * @public 107 */ 108 109exports.promiseImpl = (new Function('return this;'))().Promise; 110 111/** 112 * Get the path to the included file from the parent file path and the 113 * specified path. 114 * 115 * @param {String} name specified path 116 * @param {String} filename parent file path 117 * @param {Boolean} [isDir=false] whether the parent file path is a directory 118 * @return {String} 119 */ 120exports.resolveInclude = function(name, filename, isDir) { 121 var dirname = path.dirname; 122 var extname = path.extname; 123 var resolve = path.resolve; 124 var includePath = resolve(isDir ? filename : dirname(filename), name); 125 var ext = extname(name); 126 if (!ext) { 127 includePath += '.ejs'; 128 } 129 return includePath; 130}; 131 132/** 133 * Try to resolve file path on multiple directories 134 * 135 * @param {String} name specified path 136 * @param {Array<String>} paths list of possible parent directory paths 137 * @return {String} 138 */ 139function resolvePaths(name, paths) { 140 var filePath; 141 if (paths.some(function (v) { 142 filePath = exports.resolveInclude(name, v, true); 143 return fs.existsSync(filePath); 144 })) { 145 return filePath; 146 } 147} 148 149/** 150 * Get the path to the included file by Options 151 * 152 * @param {String} path specified path 153 * @param {Options} options compilation options 154 * @return {String} 155 */ 156function getIncludePath(path, options) { 157 var includePath; 158 var filePath; 159 var views = options.views; 160 var match = /^[A-Za-z]+:\\|^\//.exec(path); 161 162 // Abs path 163 if (match && match.length) { 164 path = path.replace(/^\/*/, ''); 165 if (Array.isArray(options.root)) { 166 includePath = resolvePaths(path, options.root); 167 } else { 168 includePath = exports.resolveInclude(path, options.root || '/', true); 169 } 170 } 171 // Relative paths 172 else { 173 // Look relative to a passed filename first 174 if (options.filename) { 175 filePath = exports.resolveInclude(path, options.filename); 176 if (fs.existsSync(filePath)) { 177 includePath = filePath; 178 } 179 } 180 // Then look in any views directories 181 if (!includePath && Array.isArray(views)) { 182 includePath = resolvePaths(path, views); 183 } 184 if (!includePath && typeof options.includer !== 'function') { 185 throw new Error('Could not find the include file "' + 186 options.escapeFunction(path) + '"'); 187 } 188 } 189 return includePath; 190} 191 192/** 193 * Get the template from a string or a file, either compiled on-the-fly or 194 * read from cache (if enabled), and cache the template if needed. 195 * 196 * If `template` is not set, the file specified in `options.filename` will be 197 * read. 198 * 199 * If `options.cache` is true, this function reads the file from 200 * `options.filename` so it must be set prior to calling this function. 201 * 202 * @memberof module:ejs-internal 203 * @param {Options} options compilation options 204 * @param {String} [template] template source 205 * @return {(TemplateFunction|ClientFunction)} 206 * Depending on the value of `options.client`, either type might be returned. 207 * @static 208 */ 209 210function handleCache(options, template) { 211 var func; 212 var filename = options.filename; 213 var hasTemplate = arguments.length > 1; 214 215 if (options.cache) { 216 if (!filename) { 217 throw new Error('cache option requires a filename'); 218 } 219 func = exports.cache.get(filename); 220 if (func) { 221 return func; 222 } 223 if (!hasTemplate) { 224 template = fileLoader(filename).toString().replace(_BOM, ''); 225 } 226 } 227 else if (!hasTemplate) { 228 // istanbul ignore if: should not happen at all 229 if (!filename) { 230 throw new Error('Internal EJS error: no file name or template ' 231 + 'provided'); 232 } 233 template = fileLoader(filename).toString().replace(_BOM, ''); 234 } 235 func = exports.compile(template, options); 236 if (options.cache) { 237 exports.cache.set(filename, func); 238 } 239 return func; 240} 241 242/** 243 * Try calling handleCache with the given options and data and call the 244 * callback with the result. If an error occurs, call the callback with 245 * the error. Used by renderFile(). 246 * 247 * @memberof module:ejs-internal 248 * @param {Options} options compilation options 249 * @param {Object} data template data 250 * @param {RenderFileCallback} cb callback 251 * @static 252 */ 253 254function tryHandleCache(options, data, cb) { 255 var result; 256 if (!cb) { 257 if (typeof exports.promiseImpl == 'function') { 258 return new exports.promiseImpl(function (resolve, reject) { 259 try { 260 result = handleCache(options)(data); 261 resolve(result); 262 } 263 catch (err) { 264 reject(err); 265 } 266 }); 267 } 268 else { 269 throw new Error('Please provide a callback function'); 270 } 271 } 272 else { 273 try { 274 result = handleCache(options)(data); 275 } 276 catch (err) { 277 return cb(err); 278 } 279 280 cb(null, result); 281 } 282} 283 284/** 285 * fileLoader is independent 286 * 287 * @param {String} filePath ejs file path. 288 * @return {String} The contents of the specified file. 289 * @static 290 */ 291 292function fileLoader(filePath){ 293 return exports.fileLoader(filePath); 294} 295 296/** 297 * Get the template function. 298 * 299 * If `options.cache` is `true`, then the template is cached. 300 * 301 * @memberof module:ejs-internal 302 * @param {String} path path for the specified file 303 * @param {Options} options compilation options 304 * @return {(TemplateFunction|ClientFunction)} 305 * Depending on the value of `options.client`, either type might be returned 306 * @static 307 */ 308 309function includeFile(path, options) { 310 var opts = utils.shallowCopy(utils.createNullProtoObjWherePossible(), options); 311 opts.filename = getIncludePath(path, opts); 312 if (typeof options.includer === 'function') { 313 var includerResult = options.includer(path, opts.filename); 314 if (includerResult) { 315 if (includerResult.filename) { 316 opts.filename = includerResult.filename; 317 } 318 if (includerResult.template) { 319 return handleCache(opts, includerResult.template); 320 } 321 } 322 } 323 return handleCache(opts); 324} 325 326/** 327 * Re-throw the given `err` in context to the `str` of ejs, `filename`, and 328 * `lineno`. 329 * 330 * @implements {RethrowCallback} 331 * @memberof module:ejs-internal 332 * @param {Error} err Error object 333 * @param {String} str EJS source 334 * @param {String} flnm file name of the EJS file 335 * @param {Number} lineno line number of the error 336 * @param {EscapeCallback} esc 337 * @static 338 */ 339 340function rethrow(err, str, flnm, lineno, esc) { 341 var lines = str.split('\n'); 342 var start = Math.max(lineno - 3, 0); 343 var end = Math.min(lines.length, lineno + 3); 344 var filename = esc(flnm); 345 // Error context 346 var context = lines.slice(start, end).map(function (line, i){ 347 var curr = i + start + 1; 348 return (curr == lineno ? ' >> ' : ' ') 349 + curr 350 + '| ' 351 + line; 352 }).join('\n'); 353 354 // Alter exception message 355 err.path = filename; 356 err.message = (filename || 'ejs') + ':' 357 + lineno + '\n' 358 + context + '\n\n' 359 + err.message; 360 361 throw err; 362} 363 364function stripSemi(str){ 365 return str.replace(/;(\s*$)/, '$1'); 366} 367 368/** 369 * Compile the given `str` of ejs into a template function. 370 * 371 * @param {String} template EJS template 372 * 373 * @param {Options} [opts] compilation options 374 * 375 * @return {(TemplateFunction|ClientFunction)} 376 * Depending on the value of `opts.client`, either type might be returned. 377 * Note that the return type of the function also depends on the value of `opts.async`. 378 * @public 379 */ 380 381exports.compile = function compile(template, opts) { 382 var templ; 383 384 // v1 compat 385 // 'scope' is 'context' 386 // FIXME: Remove this in a future version 387 if (opts && opts.scope) { 388 if (!scopeOptionWarned){ 389 console.warn('`scope` option is deprecated and will be removed in EJS 3'); 390 scopeOptionWarned = true; 391 } 392 if (!opts.context) { 393 opts.context = opts.scope; 394 } 395 delete opts.scope; 396 } 397 templ = new Template(template, opts); 398 return templ.compile(); 399}; 400 401/** 402 * Render the given `template` of ejs. 403 * 404 * If you would like to include options but not data, you need to explicitly 405 * call this function with `data` being an empty object or `null`. 406 * 407 * @param {String} template EJS template 408 * @param {Object} [data={}] template data 409 * @param {Options} [opts={}] compilation and rendering options 410 * @return {(String|Promise<String>)} 411 * Return value type depends on `opts.async`. 412 * @public 413 */ 414 415exports.render = function (template, d, o) { 416 var data = d || utils.createNullProtoObjWherePossible(); 417 var opts = o || utils.createNullProtoObjWherePossible(); 418 419 // No options object -- if there are optiony names 420 // in the data, copy them to options 421 if (arguments.length == 2) { 422 utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA); 423 } 424 425 return handleCache(opts, template)(data); 426}; 427 428/** 429 * Render an EJS file at the given `path` and callback `cb(err, str)`. 430 * 431 * If you would like to include options but not data, you need to explicitly 432 * call this function with `data` being an empty object or `null`. 433 * 434 * @param {String} path path to the EJS file 435 * @param {Object} [data={}] template data 436 * @param {Options} [opts={}] compilation and rendering options 437 * @param {RenderFileCallback} cb callback 438 * @public 439 */ 440 441exports.renderFile = function () { 442 var args = Array.prototype.slice.call(arguments); 443 var filename = args.shift(); 444 var cb; 445 var opts = {filename: filename}; 446 var data; 447 var viewOpts; 448 449 // Do we have a callback? 450 if (typeof arguments[arguments.length - 1] == 'function') { 451 cb = args.pop(); 452 } 453 // Do we have data/opts? 454 if (args.length) { 455 // Should always have data obj 456 data = args.shift(); 457 // Normal passed opts (data obj + opts obj) 458 if (args.length) { 459 // Use shallowCopy so we don't pollute passed in opts obj with new vals 460 utils.shallowCopy(opts, args.pop()); 461 } 462 // Special casing for Express (settings + opts-in-data) 463 else { 464 // Express 3 and 4 465 if (data.settings) { 466 // Pull a few things from known locations 467 if (data.settings.views) { 468 opts.views = data.settings.views; 469 } 470 if (data.settings['view cache']) { 471 opts.cache = true; 472 } 473 // Undocumented after Express 2, but still usable, esp. for 474 // items that are unsafe to be passed along with data, like `root` 475 viewOpts = data.settings['view options']; 476 if (viewOpts) { 477 utils.shallowCopy(opts, viewOpts); 478 } 479 } 480 // Express 2 and lower, values set in app.locals, or people who just 481 // want to pass options in their data. NOTE: These values will override 482 // anything previously set in settings or settings['view options'] 483 utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS); 484 } 485 opts.filename = filename; 486 } 487 else { 488 data = utils.createNullProtoObjWherePossible(); 489 } 490 491 return tryHandleCache(opts, data, cb); 492}; 493 494/** 495 * Clear intermediate JavaScript cache. Calls {@link Cache#reset}. 496 * @public 497 */ 498 499/** 500 * EJS template class 501 * @public 502 */ 503exports.Template = Template; 504 505exports.clearCache = function () { 506 exports.cache.reset(); 507}; 508 509function Template(text, opts) { 510 opts = opts || utils.createNullProtoObjWherePossible(); 511 var options = utils.createNullProtoObjWherePossible(); 512 this.templateText = text; 513 /** @type {string | null} */ 514 this.mode = null; 515 this.truncate = false; 516 this.currentLine = 1; 517 this.source = ''; 518 options.client = opts.client || false; 519 options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML; 520 options.compileDebug = opts.compileDebug !== false; 521 options.debug = !!opts.debug; 522 options.filename = opts.filename; 523 options.openDelimiter = opts.openDelimiter || exports.openDelimiter || _DEFAULT_OPEN_DELIMITER; 524 options.closeDelimiter = opts.closeDelimiter || exports.closeDelimiter || _DEFAULT_CLOSE_DELIMITER; 525 options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER; 526 options.strict = opts.strict || false; 527 options.context = opts.context; 528 options.cache = opts.cache || false; 529 options.rmWhitespace = opts.rmWhitespace; 530 options.root = opts.root; 531 options.includer = opts.includer; 532 options.outputFunctionName = opts.outputFunctionName; 533 options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME; 534 options.views = opts.views; 535 options.async = opts.async; 536 options.destructuredLocals = opts.destructuredLocals; 537 options.legacyInclude = typeof opts.legacyInclude != 'undefined' ? !!opts.legacyInclude : true; 538 539 if (options.strict) { 540 options._with = false; 541 } 542 else { 543 options._with = typeof opts._with != 'undefined' ? opts._with : true; 544 } 545 546 this.opts = options; 547 548 this.regex = this.createRegex(); 549} 550 551Template.modes = { 552 EVAL: 'eval', 553 ESCAPED: 'escaped', 554 RAW: 'raw', 555 COMMENT: 'comment', 556 LITERAL: 'literal' 557}; 558 559Template.prototype = { 560 createRegex: function () { 561 var str = _REGEX_STRING; 562 var delim = utils.escapeRegExpChars(this.opts.delimiter); 563 var open = utils.escapeRegExpChars(this.opts.openDelimiter); 564 var close = utils.escapeRegExpChars(this.opts.closeDelimiter); 565 str = str.replace(/%/g, delim) 566 .replace(/</g, open) 567 .replace(/>/g, close); 568 return new RegExp(str); 569 }, 570 571 compile: function () { 572 /** @type {string} */ 573 var src; 574 /** @type {ClientFunction} */ 575 var fn; 576 var opts = this.opts; 577 var prepended = ''; 578 var appended = ''; 579 /** @type {EscapeCallback} */ 580 var escapeFn = opts.escapeFunction; 581 /** @type {FunctionConstructor} */ 582 var ctor; 583 /** @type {string} */ 584 var sanitizedFilename = opts.filename ? JSON.stringify(opts.filename) : 'undefined'; 585 586 if (!this.source) { 587 this.generateSource(); 588 prepended += 589 ' var __output = "";\n' + 590 ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n'; 591 if (opts.outputFunctionName) { 592 if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) { 593 throw new Error('outputFunctionName is not a valid JS identifier.'); 594 } 595 prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; 596 } 597 if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) { 598 throw new Error('localsName is not a valid JS identifier.'); 599 } 600 if (opts.destructuredLocals && opts.destructuredLocals.length) { 601 var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n'; 602 for (var i = 0; i < opts.destructuredLocals.length; i++) { 603 var name = opts.destructuredLocals[i]; 604 if (!_JS_IDENTIFIER.test(name)) { 605 throw new Error('destructuredLocals[' + i + '] is not a valid JS identifier.'); 606 } 607 if (i > 0) { 608 destructuring += ',\n '; 609 } 610 destructuring += name + ' = __locals.' + name; 611 } 612 prepended += destructuring + ';\n'; 613 } 614 if (opts._with !== false) { 615 prepended += ' with (' + opts.localsName + ' || {}) {' + '\n'; 616 appended += ' }' + '\n'; 617 } 618 appended += ' return __output;' + '\n'; 619 this.source = prepended + this.source + appended; 620 } 621 622 if (opts.compileDebug) { 623 src = 'var __line = 1' + '\n' 624 + ' , __lines = ' + JSON.stringify(this.templateText) + '\n' 625 + ' , __filename = ' + sanitizedFilename + ';' + '\n' 626 + 'try {' + '\n' 627 + this.source 628 + '} catch (e) {' + '\n' 629 + ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n' 630 + '}' + '\n'; 631 } 632 else { 633 src = this.source; 634 } 635 636 if (opts.client) { 637 src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src; 638 if (opts.compileDebug) { 639 src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src; 640 } 641 } 642 643 if (opts.strict) { 644 src = '"use strict";\n' + src; 645 } 646 if (opts.debug) { 647 console.log(src); 648 } 649 if (opts.compileDebug && opts.filename) { 650 src = src + '\n' 651 + '//# sourceURL=' + sanitizedFilename + '\n'; 652 } 653 654 try { 655 if (opts.async) { 656 // Have to use generated function for this, since in envs without support, 657 // it breaks in parsing 658 try { 659 ctor = (new Function('return (async function(){}).constructor;'))(); 660 } 661 catch(e) { 662 if (e instanceof SyntaxError) { 663 throw new Error('This environment does not support async/await'); 664 } 665 else { 666 throw e; 667 } 668 } 669 } 670 else { 671 ctor = Function; 672 } 673 fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src); 674 } 675 catch(e) { 676 // istanbul ignore else 677 if (e instanceof SyntaxError) { 678 if (opts.filename) { 679 e.message += ' in ' + opts.filename; 680 } 681 e.message += ' while compiling ejs\n\n'; 682 e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n'; 683 e.message += 'https://github.com/RyanZim/EJS-Lint'; 684 if (!opts.async) { 685 e.message += '\n'; 686 e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.'; 687 } 688 } 689 throw e; 690 } 691 692 // Return a callable function which will execute the function 693 // created by the source-code, with the passed data as locals 694 // Adds a local `include` function which allows full recursive include 695 var returnedFn = opts.client ? fn : function anonymous(data) { 696 var include = function (path, includeData) { 697 var d = utils.shallowCopy(utils.createNullProtoObjWherePossible(), data); 698 if (includeData) { 699 d = utils.shallowCopy(d, includeData); 700 } 701 return includeFile(path, opts)(d); 702 }; 703 return fn.apply(opts.context, 704 [data || utils.createNullProtoObjWherePossible(), escapeFn, include, rethrow]); 705 }; 706 if (opts.filename && typeof Object.defineProperty === 'function') { 707 var filename = opts.filename; 708 var basename = path.basename(filename, path.extname(filename)); 709 try { 710 Object.defineProperty(returnedFn, 'name', { 711 value: basename, 712 writable: false, 713 enumerable: false, 714 configurable: true 715 }); 716 } catch (e) {/* ignore */} 717 } 718 return returnedFn; 719 }, 720 721 generateSource: function () { 722 var opts = this.opts; 723 724 if (opts.rmWhitespace) { 725 // Have to use two separate replace here as `^` and `$` operators don't 726 // work well with `\r` and empty lines don't work well with the `m` flag. 727 this.templateText = 728 this.templateText.replace(/[\r\n]+/g, '\n').replace(/^\s+|\s+$/gm, ''); 729 } 730 731 // Slurp spaces and tabs before <%_ and after _%> 732 this.templateText = 733 this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>'); 734 735 var self = this; 736 var matches = this.parseTemplateText(); 737 var d = this.opts.delimiter; 738 var o = this.opts.openDelimiter; 739 var c = this.opts.closeDelimiter; 740 741 if (matches && matches.length) { 742 matches.forEach(function (line, index) { 743 var closing; 744 // If this is an opening tag, check for closing tags 745 // FIXME: May end up with some false positives here 746 // Better to store modes as k/v with openDelimiter + delimiter as key 747 // Then this can simply check against the map 748 if ( line.indexOf(o + d) === 0 // If it is a tag 749 && line.indexOf(o + d + d) !== 0) { // and is not escaped 750 closing = matches[index + 2]; 751 if (!(closing == d + c || closing == '-' + d + c || closing == '_' + d + c)) { 752 throw new Error('Could not find matching close tag for "' + line + '".'); 753 } 754 } 755 self.scanLine(line); 756 }); 757 } 758 759 }, 760 761 parseTemplateText: function () { 762 var str = this.templateText; 763 var pat = this.regex; 764 var result = pat.exec(str); 765 var arr = []; 766 var firstPos; 767 768 while (result) { 769 firstPos = result.index; 770 771 if (firstPos !== 0) { 772 arr.push(str.substring(0, firstPos)); 773 str = str.slice(firstPos); 774 } 775 776 arr.push(result[0]); 777 str = str.slice(result[0].length); 778 result = pat.exec(str); 779 } 780 781 if (str) { 782 arr.push(str); 783 } 784 785 return arr; 786 }, 787 788 _addOutput: function (line) { 789 if (this.truncate) { 790 // Only replace single leading linebreak in the line after 791 // -%> tag -- this is the single, trailing linebreak 792 // after the tag that the truncation mode replaces 793 // Handle Win / Unix / old Mac linebreaks -- do the \r\n 794 // combo first in the regex-or 795 line = line.replace(/^(?:\r\n|\r|\n)/, ''); 796 this.truncate = false; 797 } 798 if (!line) { 799 return line; 800 } 801 802 // Preserve literal slashes 803 line = line.replace(/\\/g, '\\\\'); 804 805 // Convert linebreaks 806 line = line.replace(/\n/g, '\\n'); 807 line = line.replace(/\r/g, '\\r'); 808 809 // Escape double-quotes 810 // - this will be the delimiter during execution 811 line = line.replace(/"/g, '\\"'); 812 this.source += ' ; __append("' + line + '")' + '\n'; 813 }, 814 815 scanLine: function (line) { 816 var self = this; 817 var d = this.opts.delimiter; 818 var o = this.opts.openDelimiter; 819 var c = this.opts.closeDelimiter; 820 var newLineCount = 0; 821 822 newLineCount = (line.split('\n').length - 1); 823 824 switch (line) { 825 case o + d: 826 case o + d + '_': 827 this.mode = Template.modes.EVAL; 828 break; 829 case o + d + '=': 830 this.mode = Template.modes.ESCAPED; 831 break; 832 case o + d + '-': 833 this.mode = Template.modes.RAW; 834 break; 835 case o + d + '#': 836 this.mode = Template.modes.COMMENT; 837 break; 838 case o + d + d: 839 this.mode = Template.modes.LITERAL; 840 this.source += ' ; __append("' + line.replace(o + d + d, o + d) + '")' + '\n'; 841 break; 842 case d + d + c: 843 this.mode = Template.modes.LITERAL; 844 this.source += ' ; __append("' + line.replace(d + d + c, d + c) + '")' + '\n'; 845 break; 846 case d + c: 847 case '-' + d + c: 848 case '_' + d + c: 849 if (this.mode == Template.modes.LITERAL) { 850 this._addOutput(line); 851 } 852 853 this.mode = null; 854 this.truncate = line.indexOf('-') === 0 || line.indexOf('_') === 0; 855 break; 856 default: 857 // In script mode, depends on type of tag 858 if (this.mode) { 859 // If '//' is found without a line break, add a line break. 860 switch (this.mode) { 861 case Template.modes.EVAL: 862 case Template.modes.ESCAPED: 863 case Template.modes.RAW: 864 if (line.lastIndexOf('//') > line.lastIndexOf('\n')) { 865 line += '\n'; 866 } 867 } 868 switch (this.mode) { 869 // Just executing code 870 case Template.modes.EVAL: 871 this.source += ' ; ' + line + '\n'; 872 break; 873 // Exec, esc, and output 874 case Template.modes.ESCAPED: 875 this.source += ' ; __append(escapeFn(' + stripSemi(line) + '))' + '\n'; 876 break; 877 // Exec and output 878 case Template.modes.RAW: 879 this.source += ' ; __append(' + stripSemi(line) + ')' + '\n'; 880 break; 881 case Template.modes.COMMENT: 882 // Do nothing 883 break; 884 // Literal <%% mode, append as raw output 885 case Template.modes.LITERAL: 886 this._addOutput(line); 887 break; 888 } 889 } 890 // In string mode, just add the output 891 else { 892 this._addOutput(line); 893 } 894 } 895 896 if (self.opts.compileDebug && newLineCount) { 897 this.currentLine += newLineCount; 898 this.source += ' ; __line = ' + this.currentLine + '\n'; 899 } 900 } 901}; 902 903/** 904 * Escape characters reserved in XML. 905 * 906 * This is simply an export of {@link module:utils.escapeXML}. 907 * 908 * If `markup` is `undefined` or `null`, the empty string is returned. 909 * 910 * @param {String} markup Input string 911 * @return {String} Escaped string 912 * @public 913 * @func 914 * */ 915exports.escapeXML = utils.escapeXML; 916 917/** 918 * Express.js support. 919 * 920 * This is an alias for {@link module:ejs.renderFile}, in order to support 921 * Express.js out-of-the-box. 922 * 923 * @func 924 */ 925 926exports.__express = exports.renderFile; 927 928/** 929 * Version of EJS. 930 * 931 * @readonly 932 * @type {String} 933 * @public 934 */ 935 936exports.VERSION = _VERSION_STRING; 937 938/** 939 * Name for detection of EJS. 940 * 941 * @readonly 942 * @type {String} 943 * @public 944 */ 945 946exports.name = _NAME; 947 948/* istanbul ignore if */ 949if (typeof window != 'undefined') { 950 window.ejs = exports; 951} 952