ejs.js 24 KB

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