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