ejs.js 26 KB

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