Repository: mde/ejs Branch: main Commit: 2b155621f9b9 Files: 123 Total size: 175.5 KB Directory structure: gitextract_7tv75dat/ ├── .github/ │ └── workflows/ │ └── create-release.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RELEASE_NOTES_v4.md ├── RELEASE_NOTES_v5.md ├── SECURITY.md ├── benchmark/ │ └── bench-ejs.js ├── bin/ │ └── cli.js ├── docs/ │ └── jsdoc/ │ ├── cache.jsdoc │ ├── callbacks.jsdoc │ ├── fileLoader.jsdoc │ ├── options.jsdoc │ └── template-functions.jsdoc ├── eslint.config_cjs.mjs ├── eslint.config_default.mjs ├── eslint.config_esm.mjs ├── examples/ │ ├── client-compilation.html │ ├── client-injection.html │ ├── express/ │ │ ├── README.md │ │ ├── app.js │ │ ├── package.json │ │ └── views/ │ │ └── index.ejs │ ├── functions.ejs │ ├── functions.js │ ├── hello.ejs │ ├── list.ejs │ ├── list.js │ ├── output-function.ejs │ ├── output-function.js │ ├── partial.ejs │ └── slot/ │ ├── body.ejs │ ├── footer.ejs │ ├── index.ejs │ ├── index.js │ └── layout.ejs ├── jakefile.js ├── jsdoc.json ├── lib/ │ └── esm/ │ ├── ejs.js │ ├── package.json │ ├── parseargs.js │ └── utils.js ├── package.json ├── test/ │ ├── cli.js │ ├── ejs.js │ ├── fixtures/ │ │ ├── backslash.ejs │ │ ├── backslash.html │ │ ├── comments.ejs │ │ ├── comments.html │ │ ├── consecutive-tags.ejs │ │ ├── consecutive-tags.html │ │ ├── double-quote.ejs │ │ ├── double-quote.html │ │ ├── error.ejs │ │ ├── error.out │ │ ├── fail.ejs │ │ ├── hello-template.html │ │ ├── hello-world.ejs │ │ ├── include-abspath.ejs │ │ ├── include-escaped.ejs │ │ ├── include-escaped.html │ │ ├── include-expression.ejs │ │ ├── include-expression.html │ │ ├── include-nested-escape.ejs │ │ ├── include-nested-escape.html │ │ ├── include-nonexistent.ejs │ │ ├── include-root.ejs │ │ ├── include-simple.ejs │ │ ├── include-simple.html │ │ ├── include-with-error.ejs │ │ ├── include.css.ejs │ │ ├── include.css.html │ │ ├── include.ejs │ │ ├── include.html │ │ ├── include_cache.ejs │ │ ├── include_cache.html │ │ ├── includes/ │ │ │ ├── bom.ejs │ │ │ ├── escape.ejs │ │ │ ├── menu/ │ │ │ │ └── item.ejs │ │ │ └── menu-item.ejs │ │ ├── literal.ejs │ │ ├── literal.html │ │ ├── menu.ejs │ │ ├── menu.html │ │ ├── menu_var.ejs │ │ ├── messed.ejs │ │ ├── messed.html │ │ ├── newlines.ejs │ │ ├── newlines.html │ │ ├── newlines.mixed.ejs │ │ ├── newlines.mixed.html │ │ ├── no.newlines.ejs │ │ ├── no.newlines.error.ejs │ │ ├── no.newlines.html │ │ ├── no.semicolons.ejs │ │ ├── no.semicolons.html │ │ ├── para.ejs │ │ ├── pet.ejs │ │ ├── rmWhitespace.ejs │ │ ├── rmWhitespace.html │ │ ├── single-quote.ejs │ │ ├── single-quote.html │ │ ├── space-and-tab-slurp.ejs │ │ ├── space-and-tab-slurp.html │ │ ├── strict-destructuring.ejs │ │ ├── strict.ejs │ │ ├── style.css │ │ ├── user-no-with.ejs │ │ ├── user.ejs │ │ ├── user_data.json │ │ ├── views/ │ │ │ └── views-include.ejs │ │ ├── views-include.ejs │ │ ├── views-old.ejs │ │ ├── views.ejs │ │ └── with-context.ejs │ ├── mocha.opts │ ├── parseargs.js │ └── utils.js ├── tsconfig.json └── usage.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/create-release.yml ================================================ name: Create release on: push: tags: - 'v*' jobs: create-release: name: Create GitHub Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Get semver number id: get_semver env: TAG_NAME: ${{ github.ref }} run: echo "::set-output name=num::${TAG_NAME:11}" - name: Create release on GitHub API id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: "v${{ steps.get_semver.outputs.num }}" body: | Version ${{ steps.get_semver.outputs.num }} draft: false prerelease: false ================================================ FILE: .gitignore ================================================ # If you add anything here, consider also adding to .npmignore v8.log *.swp *.swo auth_info.js dist .idea/ tags nbproject/ spec/browser/autogen_suite.js node_modules tmtags *.DS_Store examples/*/log/* site/log/* .log npm-debug.log doc/ test/tmp coverage/ /ejs.js /ejs.min.js out/ pkg/ /package-lock.json ================================================ FILE: .npmignore ================================================ test/ # Copied from .gitignore v8.log *.swp *.swo auth_info.js dist .idea/ tags nbproject/ spec/browser/autogen_suite.js tmtags *.DS_Store examples/*/log/* site/log/* .log npm-debug.log doc/ coverage/ out/ pkg/ ================================================ FILE: .travis.yml ================================================ language: node_js sudo: false node_js: - "8" - "10" - "12" ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at mde@fleegix.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/3/0/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ Embedded JavaScript templates
[![Known Vulnerabilities](https://snyk.io/test/npm/ejs/badge.svg?style=flat)](https://snyk.io/test/npm/ejs) ============================= ## Security Security professionals, before reporting any security issues, please reference the SECURITY.md in this project, in particular, the following: "EJS is effectively a JavaScript runtime. Its entire job is to execute JavaScript. If you run the EJS render method without checking the inputs yourself, you are responsible for the results." In short, DO NOT submit 'vulnerabilities' that include this snippet of code: ```javascript app.get('/', (req, res) => { res.render('index', req.query); }); ``` ## Installation ```bash $ npm install ejs ``` ### Import or require Supports both CommonJS and ES Modules. ```javascript import ejs from 'ejs'; // Or const ejs = require('ejs'); ``` ### Compatibility Server: CommonJS approach (`require`) supports Node versions at least back to v0.12, likely older versions too. ES Modules approach (`import`) requires a Node version that supports ESM. CLI: Requires Node v8 or newer. Browser: EJS supports all modern browsers, but is very likely to work even in very, very old browsers. Your mileage may vary. ## Features * Control flow with `<% %>` * Escaped output with `<%= %>` (escape function configurable) * Unescaped raw output with `<%- %>` * Newline-trim mode ('newline slurping') with `-%>` ending tag * Whitespace-trim mode (slurp all whitespace) for control flow with `<%_ _%>` * Custom delimiters (e.g. `[? ?]` instead of `<% %>`) * Includes * Client-side support * Static caching of intermediate JavaScript * Static caching of templates * Complies with the [Express](http://expressjs.com) view system ## Example ```ejs <% if (user) { %>

<%= user.name %>

<% } %> ``` ## Basic usage ```javascript const template = ejs.compile(str, options); template(data); // => Rendered HTML string ejs.render(str, data, options); // => Rendered HTML string ejs.renderFile(filename, data, options, function(err, str){ // str => Rendered HTML string }); ``` It is also possible to use `ejs.render(dataAndOptions);` where you pass everything in a single object. In that case, you'll end up with local variables for all the passed options. However, be aware that your code could break if we add an option with the same name as one of your data object's properties. Therefore, we do not recommend using this shortcut. ### Important You should never give end-users unfettered access to the EJS render method, If you do so you are using EJS in an inherently un-secure way. ### Options - `cache` Compiled functions are cached, requires `filename` - `filename` The name of the file being rendered. Not required if you are using `renderFile()`. Used by `cache` to key caches, and for includes. - `root` Set template root(s) for includes with an absolute path (e.g, /file.ejs). Can be array to try to resolve include from multiple directories. - `views` An array of paths to use when resolving includes with relative paths. - `context` Function execution context - `compileDebug` When `false` no debug instrumentation is compiled - `delimiter` Character to use for inner delimiter, by default '%' - `openDelimiter` Character to use for opening delimiter, by default '<' - `closeDelimiter` Character to use for closing delimiter, by default '>' - `debug` Outputs generated function body - `strict` When set to `true`, generated function is in strict mode - `_with` Whether or not to use `with() {}` constructs. If `false` then the locals will be stored in the `locals` object. Set to `false` in strict mode. - `destructuredLocals` An array of local variables that are always destructured from the locals object, available even in strict mode. - `localsName` Name to use for the object storing local variables when not using `with` Defaults to `locals` - `rmWhitespace` Remove all safe-to-remove whitespace, including leading and trailing whitespace. It also enables a safer version of `-%>` line slurping for all scriptlet tags (it does not strip new lines of tags in the middle of a line). - `escape` The escaping function used with `<%=` construct. (By default escapes XML). - `outputFunctionName` Set to a string (e.g., 'echo' or 'print') for a function to print output inside scriptlet tags. - `async` When `true`, EJS will use an async function for rendering. (Depends on async/await support in the JS runtime). - `includer` Custom function to handle EJS includes, receives `(originalPath, parsedPath)` parameters, where `originalPath` is the path in include as-is and `parsedPath` is the previously resolved path. Should return an object `{ filename, template }`, you may return only one of the properties, where `filename` is the final parsed path and `template` is the included content. This project uses [JSDoc](https://jsdoc.app/). For the full public API documentation, clone the repository and run `jake doc`. This will run JSDoc with the proper options and output the documentation to `out/`. If you want the both the public & private API docs, run `jake devdoc` instead. ### Tags - `<%` 'Scriptlet' tag, for control-flow, no output - `<%_` 'Whitespace Slurping' Scriptlet tag, strips all whitespace before it - `<%=` Outputs the value into the template (escaped) - `<%-` Outputs the unescaped value into the template - `<%#` Comment tag, no execution, no output - `<%%` Outputs a literal '<%' - `%%>` Outputs a literal '%>' - `%>` Plain ending tag - `-%>` Trim-mode ('newline slurp') tag, trims following newline - `_%>` 'Whitespace Slurping' ending tag, removes all whitespace after it For the full syntax documentation, please see [docs/syntax.md](https://github.com/mde/ejs/blob/master/docs/syntax.md). ### Includes Includes either have to be an absolute path, or, if not, are assumed as relative to the template with the `include` call. For example if you are including `./views/user/show.ejs` from `./views/users.ejs` you would use `<%- include('user/show') %>`. You must specify the `filename` option for the template with the `include` call unless you are using `renderFile()`. You'll likely want to use the raw output tag (`<%-`) with your include to avoid double-escaping the HTML output. ```ejs ``` Includes are inserted at runtime, so you can use variables for the path in the `include` call (for example `<%- include(somePath) %>`). Variables in your top-level data object are available to all your includes, but local variables need to be passed down. NOTE: Include preprocessor directives (`<% include user/show %>`) are not supported in v3.0+. ## Custom delimiters Custom delimiters can be applied on a per-template basis, or globally: ```javascript import ejs from 'ejs'; const users = ['geddy', 'neil', 'alex']; // Just one template ejs.render('

[?= users.join(" | "); ?]

', {users: users}, {delimiter: '?', openDelimiter: '[', closeDelimiter: ']'}); // => '

geddy | neil | alex

' // Or globally ejs.delimiter = '?'; ejs.openDelimiter = '['; ejs.closeDelimiter = ']'; ejs.render('

[?= users.join(" | "); ?]

', {users: users}); // => '

geddy | neil | alex

' ``` ### Caching EJS ships with a basic in-process cache for caching the intermediate JavaScript functions used to render templates. It's easy to plug in LRU caching using Node's `lru-cache` library: ```javascript import ejs from 'ejs'; import { LRUCache } from 'lru-cache'; ejs.cache = LRUCache({max: 100}); // LRU cache with 100-item limit ``` If you want to clear the EJS cache, call `ejs.clearCache`. If you're using the LRU cache and need a different limit, simple reset `ejs.cache` to a new instance of the LRU. ### Custom file loader The default file loader is `fs.readFileSync`, if you want to customize it, you can set ejs.fileLoader. ```javascript import ejs from 'ejs'; const myFileLoad = function (filePath) { return 'myFileLoad: ' + fs.readFileSync(filePath); }; ejs.fileLoader = myFileLoad; ``` With this feature, you can preprocess the template before reading it. ### Layouts EJS does not specifically support blocks, but layouts can be implemented by including headers and footers, like so: ```ejs <%- include('header') -%>

Title

My page

<%- include('footer') -%> ``` ## Client-side support Go to the [Latest Release](https://github.com/mde/ejs/releases/latest), download `./ejs.js` or `./ejs.min.js`. Alternately, you can compile it yourself by cloning the repository and running `jake build` (or `npx jake build` if jake is not installed globally). Include one of these files on your page, and `ejs` should be available globally. ### Example ```html
``` ### Caveats Most of EJS will work as expected; however, there are a few things to note: 1. Obviously, since you do not have access to the filesystem, `ejs.renderFile()` won't work. 2. For the same reason, `include`s do not work unless you use an `include callback`. Here is an example: ```javascript let str = "Hello <%= include('file', {person: 'John'}); %>", fn = ejs.compile(str); fn(data, null, function(path, d){ // include callback // path -> 'file' // d -> {person: 'John'} // Put your code here // Return the contents of file as a string }); // returns rendered string ``` See the [examples folder](https://github.com/mde/ejs/tree/master/examples) for more details. ## CLI EJS ships with a full-featured CLI. Options are similar to those used in JavaScript code: - `-o / --output-file FILE` Write the rendered output to FILE rather than stdout. - `-f / --data-file FILE` Must be JSON-formatted. Use parsed input from FILE as data for rendering. - `-i / --data-input STRING` Must be JSON-formatted and URI-encoded. Use parsed input from STRING as data for rendering. - `-m / --delimiter CHARACTER` Use CHARACTER with angle brackets for open/close (defaults to %). - `-p / --open-delimiter CHARACTER` Use CHARACTER instead of left angle bracket to open. - `-c / --close-delimiter CHARACTER` Use CHARACTER instead of right angle bracket to close. - `-s / --strict` When set to `true`, generated function is in strict mode - `-n / --no-with` Use 'locals' object for vars rather than using `with` (implies --strict). - `-l / --locals-name` Name to use for the object storing local variables when not using `with`. - `-w / --rm-whitespace` Remove all safe-to-remove whitespace, including leading and trailing whitespace. - `-d / --debug` Outputs generated function body - `-h / --help` Display this help message. - `-V/v / --version` Display the EJS version. Here are some examples of usage: ```shell $ ejs -p [ -c ] ./template_file.ejs -o ./output.html $ ejs ./test/fixtures/user.ejs name=Lerxst $ ejs -n -l _ ./some_template.ejs -f ./data_file.json ``` ### Data input There is a variety of ways to pass the CLI data for rendering. Stdin: ```shell $ ./test/fixtures/user_data.json | ejs ./test/fixtures/user.ejs $ ejs ./test/fixtures/user.ejs < test/fixtures/user_data.json ``` A data file: ```shell $ ejs ./test/fixtures/user.ejs -f ./user_data.json ``` A command-line option (must be URI-encoded): ```shell ./bin/cli.js -i %7B%22name%22%3A%20%22foo%22%7D ./test/fixtures/user.ejs ``` Or, passing values directly at the end of the invocation: ```shell ./bin/cli.js -m $ ./test/fixtures/user.ejs name=foo ``` ### Output The CLI by default send output to stdout, but you can use the `-o` or `--output-file` flag to specify a target file to send the output to. ## IDE Integration with Syntax Highlighting VSCode:Javascript EJS by *DigitalBrainstem* ## Related projects There are a number of implementations of EJS: * TJ's implementation, the v1 of this library: https://github.com/tj/ejs * EJS Embedded JavaScript Framework on Google Code: https://code.google.com/p/embeddedjavascript/ * Sam Stephenson's Ruby implementation: https://rubygems.org/gems/ejs * Erubis, an ERB implementation which also runs JavaScript: http://www.kuwata-lab.com/erubis/users-guide.04.html#lang-javascript * DigitalBrainstem EJS Language support: https://github.com/Digitalbrainstem/ejs-grammar ## License Licensed under the Apache License, Version 2.0 () - - - EJS Embedded JavaScript templates copyright 2112 mde@fleegix.org. ================================================ FILE: RELEASE_NOTES_v4.md ================================================ # EJS Version 4.0.1 Release Notes ## Overview EJS version 4.0.1 represents a major release with significant architectural improvements, enhanced module support, and improved compatibility. The CommonJS build is now compiled using the TypeScript compiler, ensuring better code quality, maintainability, and backward compatibility. ## Major Changes ### Module System Overhaul - **Dual module support**: Added support for both CommonJS (`lib/cjs/ejs.js`) and ES Modules (`lib/esm/ejs.js`) - **Package exports**: Implemented proper `exports` field in package.json for better module resolution - **Code generation improvements**: Replaced `let` in code-generation strings for CommonJS compatibility - **Namespace Node builtins**: Improved isolation and compatibility by namespacing Node.js built-in modules ### Compatibility - **Extended Node.js support**: Maintained compatibility with Node.js versions back to 0.12.18 - **Cleaner keyword replacement**: Improved handling of JavaScript keywords in templates ### Build System - **Compilation task**: Added new compile task with updated linting configuration - **Build improvements**: Enhanced build process to run before tests - **Test infrastructure**: Added `testOnly` task for running tests without building - **Version string**: Version string is now baked in during packaging process ### Documentation - **JSDoc updates**: Complete JSDoc overhaul with updated paths and references - **Documentation fixes**: - Fixed missing closing parenthesis in async option description (#766) - Updated JSDoc reference from usejsdoc.org to jsdoc.app (#778) - **Removed outdated docs**: Cleaned up old documentation files ### Dependencies - **Development dependencies**: Updated various dev dependencies including ESLint, TypeScript, and build tools - **Removed lockfiles**: Removed package-lock.json from repository ### Code Quality - **Linting**: Updated ESLint configuration for better code quality - **Code cleanup**: Removed unused imports and cleaned up codebase - **Test fixes**: Fixed failing tests to ensure stability ## Breaking Changes ### Package Structure - **Main entry point**: Changed from `./lib/ejs.js` to `./lib/cjs/ejs.js` - **Module entry**: New `module` field points to `./lib/esm/ejs.js` - **Exports field**: New `exports` field defines import/require paths ## Contributors - mde (Matthew Eernisse) - Adnan Tahir (#766) - Thomas Skardal (#778) ## Migration Guide If you're upgrading from version 3.x: 1. **ES Modules**: Standard ESM imports continue to work as before. The `exports` field automatically routes imports to the correct module: ```javascript // Works in Node.js, Deno, and other ESM environments import ejs from 'ejs'; ``` **Deno users**: EJS is now importable via npm specifier: ```javascript import ejs from 'npm:ejs'; ``` 2. **CommonJS**: CommonJS usage continues to work as before. The `exports` field automatically routes `require()` calls to the CommonJS build: ```javascript const ejs = require('ejs'); ``` 3. **No code changes required**: The new package structure is transparent to users thanks to the `exports` field. Your existing code should work without modifications. ## Installation ```bash npm install ejs@4.0.1 ``` --- *Generated from git log: v3.1.10..v4.0.1* ================================================ FILE: RELEASE_NOTES_v5.md ================================================ # EJS Version 5.0.1 Release Notes ## Overview EJS version 5.0.1 is a major release that removes deprecated options, fixes template behavior with custom delimiters, improves the CLI and build pipeline, and simplifies the package by moving Jake to a dev-only dependency. ## Major Changes ### Deprecated Option Removed - **Removed `client` option** (Fixes #746): The legacy `client` flag and related code have been removed. This option produced browser-oriented template functions by inlining escape and rethrow helpers; it was unmaintained and broken. Use the standard browser bundle (`ejs.min.js`) or compile templates for the client using your own build setup. ### Bug Fixes - **Custom delimiters and whitespace-slurp tags** (Fixed #780): Whitespace-slurp tags (`<%_` and `_%>` by default) now respect custom `openDelimiter`, `delimiter`, and `closeDelimiter`. Previously, the slurp regex was hardcoded to `<%`/`%>`, so custom delimiters did not work correctly with `<%_`/`_%>`-style tags. ### CLI & Build - **CLI no longer depends on Jake**: The `ejs` CLI now uses a bundled argument parser (`lib/esm/parseargs.js` / `lib/cjs/parseargs.js`) instead of the Jake program module. Jake remains a devDependency for the build (lint, compile, browserify, minify, test). - **Jake moved to devDependencies**: Jake was moved from `dependencies` to `devDependencies`, so installing `ejs` as a dependency no longer pulls in Jake. - **Minification fix**: The minify task now minifies the browserified `ejs.js` bundle (output of the browserify task) instead of `lib/cjs/ejs.js`, so the browser bundle is correctly minified. ### Documentation & Examples - **JSDoc updates**: Removed references to the `client` option and `ClientFunction` from options and template-function documentation. - **Examples**: `examples/client-compilation.html` and `examples/express/app.js` updated to remove use of the `client` option; Express example no longer passes `client: true`. - **README**: Removed broken link to the third-party EJS playground. ### Code Quality - **Tests**: Removed tests that targeted the removed `client` option behavior. - **Utils**: Removed unused `client`-related code from `lib/esm/utils.js`. ## Breaking Changes ### Removed `client` Option - **Option removed**: The `client` option is no longer supported. Passing `client: true` (or any value) is ignored; no error is thrown, but no client-specific output is produced. - **Migration**: Rely on the standard API and the browser build (`ejs.min.js`) for browser use, or compile templates in your own build pipeline if you need client-side rendering with custom setup. ## Contributors - mde (Matthew Eernisse) ## Migration Guide If you're upgrading from version 4.x: 1. **If you used the `client` option**: Remove `client: true` (or similar) from your options. Use `ejs.render()` or `ejs.renderFile()` as usual; for browsers, load `ejs.min.js` or bundle the CJS/ESM build. If you depended on the old client output format, you will need to implement your own client compilation or use a different approach. 2. **If you use custom delimiters with whitespace-slurp tags**: Upgrading fixes behavior so that tags like `<%_` and `_%>` are correctly recognized when `openDelimiter`, `delimiter`, or `closeDelimiter` are set. No code changes required. 3. **Install size**: Installing `ejs` as a dependency no longer installs Jake, which may slightly reduce install size and dependency tree depth. ## Installation ```bash npm install ejs@5.0.1 ``` --- *Generated from git log: v4.0.1..v5.0.1* ================================================ FILE: SECURITY.md ================================================ # Security Policy This document outlines security procedures and general policies for the EJS template engine project ## Supported Versions The current supported version. | Version | Supported | | ------- | ------------------ | | 4.x.x | :white_check_mark: | ## Reporting a Vulnerability The EJS team and community take all security bugs in EJS seriously. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. Report security bugs by emailing the lead maintainer in the Readme.md file. To ensure the timely response to your report, please ensure that the entirety of the report is contained within the email body and not solely behind a web link or an attachment. The EJS team will then evaluate your report and will reply with the next steps in handling your report and may ask for additional information or guidance. ## Out-of-Scope Vulnerabilities If you give end-users unfettered access to the EJS render method, you are using EJS in an inherently un-secure way. Please do not report security issues that stem from doing that. EJS is effectively a JavaScript runtime. Its entire job is to execute JavaScript. If you run the EJS render method without checking the inputs yourself, you are responsible for the results. In short, DO NOT send reports including this snippet of code: ```javascript const express = require('express'); const app = express(); const PORT = 3000; app.set('views', __dirname); app.set('view engine', 'ejs'); app.get('/', (req, res) => { res.render('index', req.query); }); app.listen(PORT, ()=> { console.log(`Server is running on ${PORT}`); }); ``` ================================================ FILE: benchmark/bench-ejs.js ================================================ 'use strict'; var ejs = require('..'); var path = require('path'); ejs.fileLoader = function(n) { return files[path.basename(n, '.ejs')]; }; var loops = 10000; var runs = 9; // min 4 for median var runCompile = false; var runNoCache = false; var runCache = false; var i = 1; while (i < process.argv.length) { var a = process.argv[i]; i++; var b; if (i < process.argv.length) b = process.argv[i]; switch (a) { case '-r': if(b) runs = b; i++; break; case '-l': if(b) loops = b; i++; break; case '--compile': runCompile = true; break; case '--nocache': runNoCache = true; break; case '--cache': runCache = true; break; } } if (! (runCompile || runNoCache || runCache)) { runCompile = true; runNoCache = true; runCache = true; } var files = { bench1: `

<$= name $>

<%- num+1 -%>
<% if(num > 10) { %> <$= cheese $> <% } %> <%# comment #%> <%% literal <$= name $> %%> `, bench2: `

<$= name $>

<%- num+1 -%>
<% if(num > 10) { %> <$= cheese $> <% } %> <%# comment #%> <%% literal <$= name $> %%> `.repeat(100), simple1: `

<$= name $>

<%- num+1 -%>
`, locals1: `

<$= locals.name $>

<%- locals.num+1 -%>
<% if(locals.num > 10) { %> <$= locals.cheese $> <% } %> <%# comment #%> <%% literal <$= locals.name $> %%> `.repeat(10), include1: `

<$= name $>

<% include('/simple1') %>
<% include('/simple1') %>
<% include('/simple1') %>
`, include2: `

<$= name $>

<% include /include1 %>
<% include /simple1 %>
`, }; var data = { name: 'foo', num: 42, cheese: 'out of', }; var sp = ' '; function fill(s, l) { s=String(s); return s + sp.slice(0,l-s.length); } function fillR(s, l) { s=String(s); return sp.slice(0,l-s.length)+s; } function log(name, runTimes, totalLoops) { runTimes = runTimes.sort(function(a,b) { return a-b; }); var m = Math.trunc(runs/2); var m2 = (runs % 2 == 0) ? m-1 : m; var med1 = Math.round((runTimes[m]+runTimes[m2])/2); var med2; if (runs % 2 == 0) med2 = Math.round((runTimes[m2-1]+runTimes[m2]+runTimes[m]+runTimes[m+1])/4); else med2 = Math.round((runTimes[m-1]+runTimes[m]+runTimes[m+1])/3); var avg = Math.round(runTimes.reduce(function(a,b) {return a+b;}) / runTimes.length); console.log(fill(name +': ',30), fill(avg/1000,10), fill(med1/1000,10), fill(med2/1000,10), fill(runTimes[0]/1000,10), fill(runTimes[runTimes.length-1]/1000,10),fillR(totalLoops, 15)); } function benchRender(name, file, data, opts, benchOpts) { ejs.cache.reset(); var runTimes = []; opts = opts || {}; benchOpts = benchOpts || {}; opts.filename = file; var totalLoops = Math.round(loops * (benchOpts.loopFactor || 1)); var tmpl = files[file]; for (var r = 0; r < runs; r++) { ejs.render(tmpl, data, opts); // one run in advance var t = Date.now(); for (var i = 0; i < totalLoops; i++) { ejs.render(tmpl, data, opts); } t = Date.now() - t; runTimes.push(t); } log(name, runTimes, totalLoops); } function benchCompile(name, file, opts, benchOpts) { ejs.cache.reset(); var runTimes = []; opts = opts || {}; benchOpts = benchOpts || {}; opts.filename = file; var totalLoops = Math.round(loops * (benchOpts.loopFactor || 1)); var tmpl = files[file]; for (var r = 0; r < runs; r++) { ejs.compile(tmpl, opts); // one run in advance var t = Date.now(); for (var i = 0; i < totalLoops; i++) { ejs.compile(tmpl, opts); } t = Date.now() - t; runTimes.push(t); } log(name, runTimes, totalLoops); } if (runCompile) { console.log('Running avg accross: ', runs); console.log(fill('name: ',30), fill('avg',10), fill('med',10), fill('med/avg',10), fill('min',10), fill('max',10), fillR('loops',15)); benchCompile('single tmpl compile', 'bench1', {compileDebug: false}, { loopFactor: 2 }); benchCompile('single tmpl compile (debug)', 'bench1', {compileDebug: true}, { loopFactor: 2 }); benchCompile('large tmpl compile', 'bench2', {compileDebug: false}, { loopFactor: 0.1 }); benchCompile('include-1 compile', 'include1', {compileDebug: false}, { loopFactor: 2 }); console.log('-'); } if (runCache) { benchRender('single tmpl cached', 'bench1', data, {cache:true, compileDebug: false}, { loopFactor: 5 }); benchRender('single tmpl cached (debug)', 'bench1', data, {cache:true, compileDebug: true}, { loopFactor: 5 }); benchRender('large tmpl cached', 'bench2', data, {cache:true, compileDebug: false}, { loopFactor: 0.4 }); benchRender('include-1 cached', 'include1', data, {cache:true, compileDebug: false}, { loopFactor: 2 }); benchRender('include-2 cached', 'include2', data, {cache:true, compileDebug: false}, { loopFactor: 2 }); benchRender('locals tmpl cached "with"', 'locals1', data, {cache:true, compileDebug: false, _with: true}, { loopFactor: 3 }); benchRender('locals tmpl cached NO-"with"', 'locals1', data, {cache:true, compileDebug: false, _with: false}, { loopFactor: 3 }); console.log('-'); } if (runNoCache) { benchRender('single tmpl NO-cache', 'bench1', data, {cache:false, compileDebug: false}); benchRender('single tmpl NO-cache (debug)', 'bench1', data, {cache:false, compileDebug: true}); benchRender('large tmpl NO-cache', 'bench2', data, {cache:false, compileDebug: false}, { loopFactor: 0.1 }); benchRender('include-1 NO-cache', 'include1', data, {cache:false, compileDebug: false}); benchRender('include-2 NO-cache', 'include2', data, {cache:false, compileDebug: false}); } ================================================ FILE: bin/cli.js ================================================ #!/usr/bin/env node /* * EJS Embedded JavaScript templates * Copyright 2112 Matthew Eernisse (mde@fleegix.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ let path = require('path'); let { Parser } = require('../lib/cjs/parseargs'); let ejs = require('../lib/cjs/ejs'); let { hyphenToCamel } = require('../lib/cjs/utils'); let fs = require('fs'); let args = process.argv.slice(2); let usage = fs.readFileSync(`${__dirname}/../usage.txt`).toString(); function die(msg) { console.log(msg); process.stdout.write('', function () { process.stderr.write('', function () { process.exit(); }); }); } const CLI_OPTS = [ { full: 'output-file', abbr: 'o', expectValue: true, }, { full: 'data-file', abbr: 'f', expectValue: true, }, { full: 'data-input', abbr: 'i', expectValue: true, }, { full: 'delimiter', abbr: 'm', expectValue: true, passThrough: true, }, { full: 'open-delimiter', abbr: 'p', expectValue: true, passThrough: true, }, { full: 'close-delimiter', abbr: 'c', expectValue: true, passThrough: true, }, { full: 'strict', abbr: 's', expectValue: false, allowValue: false, passThrough: true, }, { full: 'no-with', abbr: 'n', expectValue: false, allowValue: false, }, { full: 'locals-name', abbr: 'l', expectValue: true, passThrough: true, }, { full: 'rm-whitespace', abbr: 'w', expectValue: false, allowValue: false, passThrough: true, }, { full: 'debug', abbr: 'd', expectValue: false, allowValue: false, passThrough: true, }, { full: 'help', abbr: 'h', passThrough: true, }, { full: 'version', abbr: 'V', passThrough: true, }, // Alias lowercase v { full: 'version', abbr: 'v', passThrough: true, }, ]; let preempts = { version: function () { die(ejs.VERSION); }, help: function () { die(usage); } }; let stdin = ''; process.stdin.setEncoding('utf8'); process.stdin.on('readable', () => { let chunk; while ((chunk = process.stdin.read()) !== null) { stdin += chunk; } }); function run() { let parser = new Parser(CLI_OPTS); let result = parser.parse(args); let templatePath = result.taskNames[0]; let pVals = result.envVars; let pOpts = {}; for (let p in result.opts) { let name = hyphenToCamel(p); pOpts[name] = result.opts[p]; } let opts = {}; let vals = {}; // Same-named 'passthrough' opts CLI_OPTS.forEach((opt) => { let optName = hyphenToCamel(opt.full); if (opt.passThrough && typeof pOpts[optName] != 'undefined') { opts[optName] = pOpts[optName]; } }); // Bail out for help/version for (let p in opts) { if (preempts[p]) { return preempts[p](); } } // Ensure there's a template to render if (!templatePath) { throw new Error('Please provide a template path. (Run ejs -h for help)'); } if (opts.strict) { pOpts.noWith = true; } if (pOpts.noWith) { opts._with = false; } // Grab and parse any input data, in order of precedence: // 1. Stdin // 2. CLI arg via -i // 3. Data file via -f // Any individual vals passed at the end (e.g., foo=bar) will override // any vals previously set let input; let err = new Error('Please do not pass data multiple ways. Pick one of stdin, -f, or -i.'); if (stdin) { input = stdin; } else if (pOpts.dataInput) { if (input) { throw err; } input = decodeURIComponent(pOpts.dataInput); } else if (pOpts.dataFile) { if (input) { throw err; } input = fs.readFileSync(pOpts.dataFile).toString(); } if (input) { vals = JSON.parse(input); } // Override / set any individual values passed from the command line for (let p in pVals) { vals[p] = pVals[p]; } opts.filename = path.resolve(process.cwd(), templatePath); let template = fs.readFileSync(opts.filename).toString(); let output = ejs.render(template, vals, opts); if (pOpts.outputFile) { fs.writeFileSync(pOpts.outputFile, output); } else { process.stdout.write(output); } process.exit(); } // Defer execution so that stdin can be read if necessary setImmediate(run); ================================================ FILE: docs/jsdoc/cache.jsdoc ================================================ /** * A JavaScript function cache. This is implemented by the lru-cache module * on NPM, so you can simply do `ejs.cache = LRU(10)` to get a * least-recently-used cache. * * @interface Cache * @global */ /** * Cache the intermediate JavaScript function for a template. * * @function * @name Cache#set * @param {String} key key for caching * @param {Function} val cached function */ /** * Get the cached intermediate JavaScript function for a template. * * If the cache does not contain the specified key, `null` shall be returned. * * @function * @name Cache#get * @param {String} key key for caching * @return {null|Function} */ /** * Reset the entire cache. * * Erases the entire cache. Called by {@link module:ejs.clearCache} * * @function * @name Cache#reset */ ================================================ FILE: docs/jsdoc/callbacks.jsdoc ================================================ /** * Callback for receiving data from {@link module:ejs.renderFile}. * * @callback RenderFileCallback * @param {?Error} err error, if any resulted from the rendering process * @param {?String} [str] output string, is `null` or `undefined` if there is an error * @static * @global */ ================================================ FILE: docs/jsdoc/fileLoader.jsdoc ================================================ /** * A file read function, similar to fs.readFileSync, this function * can be read a file by path, return a string after processing * * @function * @name fileLoader * @param {String} path the path of the file to be read * @return {String|Object} the contents of the file as a string or objects that implement the toString() method * @global */ ================================================ FILE: docs/jsdoc/options.jsdoc ================================================ /** * Compilation and rendering options. * * @typedef Options * @type {Object} * * @property {Boolean} [debug=false] * Log generated JavaScript source for the EJS template to the console. * * @property {Boolean} [compileDebug=true] * Include additional runtime debugging information in generated template * functions. * * @property {Boolean} [_with=true] * Whether or not to use `with () {}` construct in the generated template * functions. If set to `false`, data is still accessible through the object * whose name is specified by {@link module:ejs.localsName} (default to * `locals`). * * @property {Boolean} [strict=false] * Whether to run in strict mode or not. * Enforces `_with=false`. * * @property {String[]} [destructuredLocals=[]] * An array of local variables that are always destructured from {@link module:ejs.localsName}, * available even in strict mode. * * @property {Boolean} [rmWhitespace=false] * Remove all safe-to-remove whitespace, including leading and trailing * whitespace. It also enables a safer version of `-%>` line slurping for all * scriptlet tags (it does not strip new lines of tags in the middle of a * line). * * @property {EscapeCallback} [escape={@link module:utils.escapeXML}] * The escaping function used with `<%=` construct. * * @property {String} [filename=undefined] * The filename of the template. Required for inclusion and caching unless * you are using {@link module:ejs.renderFile}. Also used for error reporting. * * @property {String|String[]} [root=undefined] * The path to the template root(s). When this is set, absolute paths for includes * (/filename.ejs) will be relative to the template root(s). * * @property {String} [openDelimiter='<'] * The opening delimiter for all statements. This allows you to clearly delinate * the difference between template code and existing delimiters. (It is recommended * to synchronize this with the closeDelimiter property.) * * @property {String} [closeDelimiter='>'] * The closing delimiter for all statements. This allows to to clearly delinate * the difference between template code and existing delimiters. (It is recommended * to synchronize this with the openDelimiter property.) * * @property {String} [delimiter='%'] * The delimiter used in template compilation. * * @property {Boolean} [cache=false] * Whether or not to enable caching of template functions. Beware that * the options of compilation are not checked as being the same, so * special handling is required if, for example, you want to cache * functions compiled with different options for the same file. * * Requires `filename` to be set. Only works with rendering function. * * @property {Object} [context=this] * The Object to which `this` is set during rendering. * * @property {Object} [scope=this] * Alias of `context`. Deprecated. * * @property {Boolean} [async=false] * Whether or not to create an async function instead of a regular function. * This requires language support. * * @static * @global */ ================================================ FILE: docs/jsdoc/template-functions.jsdoc ================================================ /** * This type of function is returned from {@link module:ejs.compile}. * * @callback TemplateFunction * @param {Object} [locals={}] * an object of data to be passed into the template. The name of this variable * is adjustable through {@link module:ejs.localsName}. * @return {(String|Promise)} * Return type depends on {@link Options}`.async`. * @static * @global */ /** * Escapes a string using HTML/XML escaping rules. * * @callback EscapeCallback * @param {String} markup Input string * @return {String} Escaped string * @static * @global */ /** * This type of callback is used when {@link Options}`.compileDebug` * is true, and an error in the template is thrown. By default it is used to * rethrow an error in a better-formatted way. * * @callback RethrowCallback * @param {Error} err Error object * @param {String} str full EJS source * @param {String} filename file name of the EJS file * @param {Number} lineno line number of the error * @param {EscapeCallback} esc * @static * @global */ /** * The callback used to include files at runtime with `include()` * * @callback IncludeCallback * @param {String} path Path to be included * @param {Object} [data] Data passed to the template * @return {String} Contents of the file requested * @static * @global */ ================================================ FILE: eslint.config_cjs.mjs ================================================ import configDefaults from "./eslint.config_default.mjs"; configDefaults[0].languageOptions.parserOptions.sourceType = "script"; export default configDefaults; ================================================ FILE: eslint.config_default.mjs ================================================ import babelParser from "@babel/eslint-parser"; export default [ { languageOptions: { parser: babelParser, globals: { "suite": "readonly", "test": "readonly" }, "parserOptions": { "ecmaVersion": 6, "sourceType": "script", "requireConfigFile": false, }, }, rules: { "linebreak-style": [ "error", "unix" ], "no-trailing-spaces": 2, "indent": [ "error", 2 ], "quotes": [ "error", "single", { "avoidEscape": true, "allowTemplateLiterals": true } ], "semi": [ "error", "always" ], "comma-style": [ "error", "last" ], "no-console": 0, "no-useless-escape": 0 } } ] ================================================ FILE: eslint.config_esm.mjs ================================================ import configDefaults from "./eslint.config_default.mjs"; configDefaults[0].languageOptions.parserOptions.sourceType = "module"; export default configDefaults; ================================================ FILE: examples/client-compilation.html ================================================ EJS compilation demo

Input template

Compilation options (JSON)

Locals (JSON)

Output



    
    
  



================================================
FILE: examples/client-injection.html
================================================



  
    
    
    
    
  
  
  



================================================
FILE: examples/express/README.md
================================================
```
npm install
npm start
open http://localhost:3000
```


================================================
FILE: examples/express/app.js
================================================
var express = require('express');
var path = require('path');
var ejs = require('ejs');

var app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

function compileEjsTemplate(name, template) {
  const compiledTemplate = ejs.compile(template, {
    outputFunctionName: name
  });
  return function compileEjsTemplate(req, res, next) {
    res.locals.compiledEjsTemplates = res.locals.compiledEjsTemplates || {};
    res.locals.compiledEjsTemplates[name] = compiledTemplate.toString();
    return next();
  };
}

app.use(compileEjsTemplate('helloTemplate', 'Hello <%= include(\'messageTemplate\', { person: \'John\' }); %>'));
app.use(compileEjsTemplate('messageTemplate', '<%= person %> now you know <%= fact %>.'));
app.use('/', function(req, res) {
  return res.render('index', {});
});

app.listen(process.env.PORT || 3000);


================================================
FILE: examples/express/package.json
================================================
{
  "description": "client side ejs compiled with express middleware",
  "main": "app.js",
  "scripts": {
    "start": "node ./app.js"
  },
  "dependencies": {
    "ejs": "^3.1.8",
    "express": "~4.16.0"
  }
}


================================================
FILE: examples/express/views/index.ejs
================================================


  
    client side ejs compiled with express middleware
  
  
    
================================================ FILE: examples/functions.ejs ================================================

Users

<% function user(user) { %>
  • <%= user.name %> is a <%= user.age %> year old <%= user.species %>.
  • <% } %> ================================================ FILE: examples/functions.js ================================================ /* * Believe it or not, you can declare and use functions in EJS templates too. */ var ejs = require('../'); var read = require('fs').readFileSync; var join = require('path').join; var path = join(__dirname, '/functions.ejs'); var data = { users: [ { name: 'Tobi', age: 2, species: 'ferret' }, { name: 'Loki', age: 2, species: 'ferret' }, { name: 'Jane', age: 6, species: 'ferret' } ] }; var ret = ejs.compile(read(path, 'utf8'), {filename: path})(data); console.log(ret); ================================================ FILE: examples/hello.ejs ================================================ Hello EJS! ================================================ FILE: examples/list.ejs ================================================ <% if (names.length) { %> <% } %> ================================================ FILE: examples/list.js ================================================ /* * This example demonstrates how to use Array.prototype.forEach() in an EJS * template. */ var ejs = require('../'); var read = require('fs').readFileSync; var join = require('path').join; var str = read(join(__dirname, '/list.ejs'), 'utf8'); var ret = ejs.compile(str)({ names: ['foo', 'bar', 'baz'] }); console.log(ret); ================================================ FILE: examples/output-function.ejs ================================================ ================================================ FILE: examples/output-function.js ================================================ /* * Believe it or not, you can declare and use functions in EJS templates too. */ var ejs = require('../'); var read = require('fs').readFileSync; var join = require('path').join; var path = join(__dirname, '/output-function.ejs'); var data = { users: [ {name: 'Tobi', age: 2, species: 'ferret'}, {name: 'Loki', age: 2, species: 'ferret'}, {name: 'Jane', age: 6, species: 'ferret'} ] }; var ret = ejs.compile(read(path, 'utf8'), { root: [join(__dirname, '..'), __dirname], filename: path, outputFunctionName: 'echo' })(data); console.log(ret); ================================================ FILE: examples/partial.ejs ================================================
  • <%= user.name %> is a <%= user.age %> year old <%= user.species %>
  • ================================================ FILE: examples/slot/body.ejs ================================================
    body
    ================================================ FILE: examples/slot/footer.ejs ================================================
    footer
    ================================================ FILE: examples/slot/index.ejs ================================================
    <%-include('./layout.ejs', { body: include('./body.ejs'), footer: include('./footer.ejs') })%>

    <%-include('./layout.ejs', { footer: include('./footer.ejs') })%>
    ================================================ FILE: examples/slot/index.js ================================================ /* * Advanced use of "include", fast layout, and dynamic rendering components. */ var ejs = require('../../lib/ejs'); var read = require('fs').readFileSync; var join = require('path').join; var path = join(__dirname, '/index.ejs'); var ret = ejs.compile(read(path, 'utf8'), {filename: path})({title: 'use slot'}); console.log(ret); ================================================ FILE: examples/slot/layout.ejs ================================================ layout

    <%=title%>

    <% if (typeof body !== 'undefined') { %> <%- body %> <% } else { %>

    This is the default body content.

    <% } %>
    description
    <%- footer %>
    ================================================ FILE: jakefile.js ================================================ let fs = require('fs'); let path = require('path'); let execSync = require('child_process').execSync; let exec = function (cmd) { execSync(cmd, {stdio: 'inherit'}); }; /* global jake, task, desc, publishTask */ const BUILT_EJS_FILES = [ 'ejs.js', 'ejs.min.js', 'lib/esm/ejs.js', 'lib/cjs/ejs.js', ]; // Hook into some of the publish lifecycle events jake.on('finished', function (ev) { switch (ev.name) { case 'publish': console.log('Updating hosted docs...'); console.log('If this fails, run jake docPublish to re-try.'); jake.Task.docPublish.invoke(); break; default: // Do nothing } }); desc('Builds the EJS library'); task('build', ['lint', 'clean', 'compile', 'browserify', 'minify']); desc('Compiles ESM to CJS source files'); task('compile', function () { // Compile CJS version exec('npx tsc'); let source = fs.readFileSync('lib/cjs/ejs.js', 'utf8').toString(); // Browerify chokes on the 'node:' prefix in require statements // Added the 'node:' prefix to ESM for Deno compat ['fs', 'path', 'url'].forEach((mod) => { source = source.replace(`require("node:${mod}")`, `require("${mod}")`); source = source.replace(new RegExp(`node_${mod}_1`, 'g'), `${mod}_1`); }); // replace `let` in code-generation strings source = source.replace( "var DECLARATION_KEYWORD = 'let';", "var DECLARATION_KEYWORD = 'var';" ); fs.writeFileSync('lib/cjs/ejs.js', source); fs.writeFileSync('lib/cjs/package.json', '{"type":"commonjs"}'); }); desc('Cleans browerified/minified files and package files'); task('clean', ['clobber'], function () { jake.rmRf('./ejs.js'); jake.rmRf('./ejs.min.js'); jake.rmRf('./lib/cjs'); console.log('Cleaned up compiled files.'); }); desc('Lints the source code'); task('lint', ['clean'], function () { let epath = path.join('./node_modules/.bin/eslint'); // Handle both ESM and CJS files in project exec(epath+' --config ./eslint.config_esm.mjs "lib/esm/*.js"'); exec(epath+' --config ./eslint.config_cjs.mjs "test/*.js" "bin/cli.js" "jakefile.js"'); console.log('Linting completed.'); }); task('browserify', function () { const currentDir = process.cwd(); process.chdir('./lib/cjs'); let epath = path.join('../../node_modules/browserify/bin/cmd.js'); exec(epath+' --standalone ejs ejs.js > ../../ejs.js'); process.chdir(currentDir); console.log('Browserification completed.'); }); task('minify', function () { let epath = path.join('./node_modules/uglify-js/bin/uglifyjs'); exec(epath+' ./ejs.js > ejs.min.js'); console.log('Minification completed.'); }); desc('Generates the EJS API docs for the public API'); task('doc', function () { jake.rmRf('out'); let epath = path.join('./node_modules/.bin/jsdoc'); exec(epath+' --verbose -c jsdoc.json lib/esm/* docs/jsdoc/*'); console.log('Documentation generated in ./out.'); }); desc('Generates the EJS API docs for the public and private API'); task('devdoc', function () { jake.rmRf('out'); let epath = path.join('./node_modules/.bin/jsdoc'); exec(epath+' --verbose -p -c jsdoc.json lib/esm/* docs/jsdoc/*'); console.log('Documentation generated in ./out.'); }); desc('Publishes the EJS API docs'); task('docPublish', ['doc'], function () { fs.writeFileSync('out/CNAME', 'api.ejs.co'); console.log('Pushing docs to gh-pages...'); let epath = path.join('./node_modules/.bin/git-directory-deploy'); exec(epath+' --directory out/'); console.log('Docs published to gh-pages.'); }); desc('Runs the EJS test suite'); task('test', ['build', 'testOnly'], function () {}); task('testOnly', function () { exec(path.join('./node_modules/.bin/mocha --u tdd')); }); publishTask('ejs', ['build'], function () { this.packageFiles.include([ 'jakefile.js', 'README.md', 'LICENSE', 'package.json', 'ejs.js', 'ejs.min.js', 'lib/**', 'bin/**', 'usage.txt' ]); }); ================================================ FILE: jsdoc.json ================================================ { "_comment": "Configuration file for JSDoc." , "tags": { "allowUnknownTags": true } , "source": { "includePattern": ".+\\.js(doc)?$" , "excludePattern": "(^|\\/|\\\\)_" } , "plugins": [ "plugins/markdown" ] , "templates": { "cleverLinks": true , "monospaceLinks": false } } ================================================ FILE: lib/esm/ejs.js ================================================ /* * EJS Embedded JavaScript templates * Copyright 2112 Matthew Eernisse (mde@fleegix.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ 'use strict'; import fs from 'node:fs'; import path from 'node:path'; import utils from './utils.js'; /** * @file Embedded JavaScript templating engine. {@link http://ejs.co} * @author Matthew Eernisse * @project EJS * @license {@link http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0} */ /** * EJS internal functions. * * Technically this "module" lies in the same file as {@link module:ejs}, for * the sake of organization all the private functions re grouped into this * module. * * @module ejs-internal * @private */ /** * Embedded JavaScript templating engine. * * @module ejs * @public */ // Keyword used in code generation -- updated to 'var' in CJS build const DECLARATION_KEYWORD = 'let'; const ejs = {}; /** @type {string} */ let _DEFAULT_OPEN_DELIMITER = '<'; let _DEFAULT_CLOSE_DELIMITER = '>'; let _DEFAULT_DELIMITER = '%'; let _DEFAULT_LOCALS_NAME = 'locals'; let _REGEX_STRING = '(<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)'; let _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug', '_with', 'rmWhitespace', 'strict', 'filename', 'async']; // We don't allow 'cache' option to be passed in the data obj for // the normal `render` call, but this is where Express 2 & 3 put it // so we make an exception for `renderFile` let _OPTS_PASSABLE_WITH_DATA_EXPRESS = _OPTS_PASSABLE_WITH_DATA.concat('cache'); let _BOM = /^\uFEFF/; let _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; /** * EJS template function cache. This can be a LRU object from lru-cache NPM * module. By default, it is {@link module:utils.cache}, a simple in-process * cache that grows continuously. * * @type {Cache} */ ejs.cache = utils.cache; /** * Custom file loader. Useful for template preprocessing or restricting access * to a certain part of the filesystem. * * @type {fileLoader} */ ejs.fileLoader = fs.readFileSync; /** * Name of the object containing the locals. * * This variable is overridden by {@link Options}`.localsName` if it is not * `undefined`. * * @type {String} * @public */ ejs.localsName = _DEFAULT_LOCALS_NAME; /** * Promise implementation -- defaults to the native implementation if available * This is mostly just for testability * * @type {PromiseConstructorLike} * @public */ ejs.promiseImpl = (new Function('return this;'))().Promise; /** * Get the path to the included file from the parent file path and the * specified path. * * @param {String} name specified path * @param {String} filename parent file path * @param {Boolean} [isDir=false] whether the parent file path is a directory * @return {String} */ ejs.resolveInclude = function(name, filename, isDir) { let dirname = path.dirname; let extname = path.extname; let resolve = path.resolve; let includePath = resolve(isDir ? filename : dirname(filename), name); let ext = extname(name); if (!ext) { includePath += '.ejs'; } return includePath; }; /** * Try to resolve file path on multiple directories * * @param {String} name specified path * @param {Array} paths list of possible parent directory paths * @return {String} */ function resolvePaths(name, paths) { let filePath; if (paths.some(function (v) { filePath = ejs.resolveInclude(name, v, true); return fs.existsSync(filePath); })) { return filePath; } } /** * Get the path to the included file by Options * * @param {String} path specified path * @param {Options} options compilation options * @return {String} */ function getIncludePath(path, options) { let includePath; let filePath; let views = options.views; let match = /^[A-Za-z]+:\\|^\//.exec(path); // Abs path if (match && match.length) { path = path.replace(/^\/*/, ''); if (Array.isArray(options.root)) { includePath = resolvePaths(path, options.root); } else { includePath = ejs.resolveInclude(path, options.root || '/', true); } } // Relative paths else { // Look relative to a passed filename first if (options.filename) { filePath = ejs.resolveInclude(path, options.filename); if (fs.existsSync(filePath)) { includePath = filePath; } } // Then look in any views directories if (!includePath && Array.isArray(views)) { includePath = resolvePaths(path, views); } if (!includePath && typeof options.includer !== 'function') { throw new Error('Could not find the include file "' + options.escapeFunction(path) + '"'); } } return includePath; } /** * Get the template from a string or a file, either compiled on-the-fly or * read from cache (if enabled), and cache the template if needed. * * If `template` is not set, the file specified in `options.filename` will be * read. * * If `options.cache` is true, this function reads the file from * `options.filename` so it must be set prior to calling this function. * * @memberof module:ejs-internal * @param {Options} options compilation options * @param {String} [template] template source * @return {TemplateFunction} * @static */ function handleCache(options, template) { let func; let filename = options.filename; let hasTemplate = arguments.length > 1; if (options.cache) { if (!filename) { throw new Error('cache option requires a filename'); } func = ejs.cache.get(filename); if (func) { return func; } if (!hasTemplate) { template = fileLoader(filename).toString().replace(_BOM, ''); } } else if (!hasTemplate) { // istanbul ignore if: should not happen at all if (!filename) { throw new Error('Internal EJS error: no file name or template ' + 'provided'); } template = fileLoader(filename).toString().replace(_BOM, ''); } func = ejs.compile(template, options); if (options.cache) { ejs.cache.set(filename, func); } return func; } /** * Try calling handleCache with the given options and data and call the * callback with the result. If an error occurs, call the callback with * the error. Used by renderFile(). * * @memberof module:ejs-internal * @param {Options} options compilation options * @param {Object} data template data * @param {RenderFileCallback} cb callback * @static */ function tryHandleCache(options, data, cb) { let result; if (!cb) { if (typeof ejs.promiseImpl == 'function') { return new ejs.promiseImpl(function (resolve, reject) { try { result = handleCache(options)(data); resolve(result); } catch (err) { reject(err); } }); } else { throw new Error('Please provide a callback function'); } } else { try { result = handleCache(options)(data); } catch (err) { return cb(err); } cb(null, result); } } /** * fileLoader is independent * * @param {String} filePath ejs file path. * @return {String} The contents of the specified file. * @static */ function fileLoader(filePath){ return ejs.fileLoader(filePath); } /** * Get the template function. * * If `options.cache` is `true`, then the template is cached. * * @memberof module:ejs-internal * @param {String} path path for the specified file * @param {Options} options compilation options * @return {TemplateFunction} * @static */ function includeFile(path, options) { let opts = utils.shallowCopy(utils.createNullProtoObjWherePossible(), options); opts.filename = getIncludePath(path, opts); if (typeof options.includer === 'function') { let includerResult = options.includer(path, opts.filename); if (includerResult) { if (includerResult.filename) { opts.filename = includerResult.filename; } if (includerResult.template) { return handleCache(opts, includerResult.template); } } } return handleCache(opts); } /** * Re-throw the given `err` in context to the `str` of ejs, `filename`, and * `lineno`. * * @implements {RethrowCallback} * @memberof module:ejs-internal * @param {Error} err Error object * @param {String} str EJS source * @param {String} flnm file name of the EJS file * @param {Number} lineno line number of the error * @param {EscapeCallback} esc * @static */ function rethrow(err, str, flnm, lineno, esc) { let lines = str.split('\n'); let start = Math.max(lineno - 3, 0); let end = Math.min(lines.length, lineno + 3); let filename = esc(flnm); // Error context let context = lines.slice(start, end).map(function (line, i){ let curr = i + start + 1; return (curr == lineno ? ' >> ' : ' ') + curr + '| ' + line; }).join('\n'); // Alter exception message err.path = filename; err.message = (filename || 'ejs') + ':' + lineno + '\n' + context + '\n\n' + err.message; throw err; } function stripSemi(str){ return str.replace(/;(\s*$)/, '$1'); } /** * Compile the given `str` of ejs into a template function. * * @param {String} template EJS template * * @param {Options} [opts] compilation options * * @return {TemplateFunction} * Note that the return type of the function depends on the value of `opts.async`. * @public */ ejs.compile = function compile(template, opts) { let templ; // v1 compat // 'scope' is 'context' // FIXME: Remove this in a future version if (opts && opts.scope) { console.warn('`scope` option is deprecated and will be removed in future EJS'); if (!opts.context) { opts.context = opts.scope; } delete opts.scope; } templ = new Template(template, opts); return templ.compile(); }; /** * Render the given `template` of ejs. * * If you would like to include options but not data, you need to explicitly * call this function with `data` being an empty object or `null`. * * @param {String} template EJS template * @param {Object} [data={}] template data * @param {Options} [opts={}] compilation and rendering options * @return {(String|Promise)} * Return value type depends on `opts.async`. * @public */ ejs.render = function (template, d, o) { let data = d || utils.createNullProtoObjWherePossible(); let opts = o || utils.createNullProtoObjWherePossible(); // No options object -- if there are optiony names // in the data, copy them to options if (arguments.length == 2) { utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA); } return handleCache(opts, template)(data); }; /** * Render an EJS file at the given `path` and callback `cb(err, str)`. * * If you would like to include options but not data, you need to explicitly * call this function with `data` being an empty object or `null`. * * @param {String} path path to the EJS file * @param {Object} [data={}] template data * @param {Options} [opts={}] compilation and rendering options * @param {RenderFileCallback} cb callback * @public */ ejs.renderFile = function () { let args = Array.prototype.slice.call(arguments); let filename = args.shift(); let cb; let opts = {filename: filename}; let data; let viewOpts; // Do we have a callback? if (typeof arguments[arguments.length - 1] == 'function') { cb = args.pop(); } // Do we have data/opts? if (args.length) { // Should always have data obj data = args.shift(); // Normal passed opts (data obj + opts obj) if (args.length) { // Use shallowCopy so we don't pollute passed in opts obj with new vals utils.shallowCopy(opts, args.pop()); } // Special casing for Express (settings + opts-in-data) else { // Express 3 and 4 if (data.settings) { // Pull a few things from known locations if (data.settings.views) { opts.views = data.settings.views; } if (data.settings['view cache']) { opts.cache = true; } // Undocumented after Express 2, but still usable, esp. for // items that are unsafe to be passed along with data, like `root` viewOpts = data.settings['view options']; if (viewOpts) { utils.shallowCopy(opts, viewOpts); } } // Express 2 and lower, values set in app.locals, or people who just // want to pass options in their data. NOTE: These values will override // anything previously set in settings or settings['view options'] utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS); } opts.filename = filename; } else { data = utils.createNullProtoObjWherePossible(); } return tryHandleCache(opts, data, cb); }; /** * Clear intermediate JavaScript cache. Calls {@link Cache#reset}. * @public */ /** * EJS template class * @public */ ejs.Template = Template; ejs.clearCache = function () { ejs.cache.reset(); }; function Template(text, optsParam) { let opts = utils.hasOwnOnlyObject(optsParam); let options = utils.createNullProtoObjWherePossible(); this.templateText = text; /** @type {string | null} */ this.mode = null; this.truncate = false; this.currentLine = 1; this.source = ''; options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML; options.compileDebug = opts.compileDebug !== false; options.debug = !!opts.debug; options.filename = opts.filename; options.openDelimiter = opts.openDelimiter || ejs.openDelimiter || _DEFAULT_OPEN_DELIMITER; options.closeDelimiter = opts.closeDelimiter || ejs.closeDelimiter || _DEFAULT_CLOSE_DELIMITER; options.delimiter = opts.delimiter || ejs.delimiter || _DEFAULT_DELIMITER; options.strict = opts.strict || false; options.context = opts.context; options.cache = opts.cache || false; options.rmWhitespace = opts.rmWhitespace; options.root = opts.root; options.includer = opts.includer; options.outputFunctionName = opts.outputFunctionName; options.localsName = opts.localsName || ejs.localsName || _DEFAULT_LOCALS_NAME; options.views = opts.views; options.async = opts.async; options.destructuredLocals = opts.destructuredLocals; options.legacyInclude = typeof opts.legacyInclude != 'undefined' ? !!opts.legacyInclude : true; if (options.strict) { options._with = false; } else { options._with = typeof opts._with != 'undefined' ? opts._with : true; } this.opts = options; this.regex = this.createRegex(); } Template.modes = { EVAL: 'eval', ESCAPED: 'escaped', RAW: 'raw', COMMENT: 'comment', LITERAL: 'literal' }; Template.prototype = { createRegex: function () { let str = _REGEX_STRING; let delim = utils.escapeRegExpChars(this.opts.delimiter); let open = utils.escapeRegExpChars(this.opts.openDelimiter); let close = utils.escapeRegExpChars(this.opts.closeDelimiter); str = str.replace(/%/g, delim) .replace(//g, close); return new RegExp(str); }, compile: function () { /** @type {string} */ let src; let fn; let opts = this.opts; let prepended = ''; let appended = ''; /** @type {EscapeCallback} */ let escapeFn = opts.escapeFunction; /** @type {FunctionConstructor} */ let ctor; /** @type {string} */ let sanitizedFilename = opts.filename ? JSON.stringify(opts.filename) : 'undefined'; if (!this.source) { this.generateSource(); prepended += ` ${DECLARATION_KEYWORD} __output = "";\n` + ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n'; if (opts.outputFunctionName) { if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) { throw new Error('outputFunctionName is not a valid JS identifier.'); } prepended += ` ${DECLARATION_KEYWORD} ` + opts.outputFunctionName + ' = __append;' + '\n'; } if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) { throw new Error('localsName is not a valid JS identifier.'); } if (opts.destructuredLocals && opts.destructuredLocals.length) { let destructuring = ` ${DECLARATION_KEYWORD} __locals = (` + opts.localsName + ' || {}),\n'; for (let i = 0; i < opts.destructuredLocals.length; i++) { let name = opts.destructuredLocals[i]; if (!_JS_IDENTIFIER.test(name)) { throw new Error('destructuredLocals[' + i + '] is not a valid JS identifier.'); } if (i > 0) { destructuring += ',\n '; } destructuring += name + ' = __locals.' + name; } prepended += destructuring + ';\n'; } if (opts._with !== false) { prepended += ' with (' + opts.localsName + ' || {}) {' + '\n'; appended += ' }' + '\n'; } appended += ' return __output;' + '\n'; this.source = prepended + this.source + appended; } if (opts.compileDebug) { src = `${DECLARATION_KEYWORD} __line = 1` + '\n' + ' , __lines = ' + JSON.stringify(this.templateText) + '\n' + ' , __filename = ' + sanitizedFilename + ';' + '\n' + 'try {' + '\n' + this.source + '} catch (e) {' + '\n' + ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n' + '}' + '\n'; } else { src = this.source; } if (opts.strict) { src = '"use strict";\n' + src; } if (opts.debug) { console.log(src); } if (opts.compileDebug && opts.filename) { src = src + '\n' + '//# sourceURL=' + sanitizedFilename + '\n'; } try { if (opts.async) { // Have to use generated function for this, since in envs without support, // it breaks in parsing try { ctor = (new Function('return (async function(){}).constructor;'))(); } catch(e) { if (e instanceof SyntaxError) { throw new Error('This environment does not support async/await'); } else { throw e; } } } else { ctor = Function; } fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src); } catch(e) { // istanbul ignore else if (e instanceof SyntaxError) { if (opts.filename) { e.message += ' in ' + opts.filename; } e.message += ' while compiling ejs\n\n'; e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n'; e.message += 'https://github.com/RyanZim/EJS-Lint'; if (!opts.async) { e.message += '\n'; e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.'; } } throw e; } // Return a callable function which will execute the function // created by the source-code, with the passed data as locals // Adds a local `include` function which allows full recursive include let returnedFn = function anonymous(data) { let include = function (path, includeData) { let d = utils.shallowCopy(utils.createNullProtoObjWherePossible(), data); if (includeData) { d = utils.shallowCopy(d, includeData); } return includeFile(path, opts)(d); }; return fn.apply(opts.context, [data || utils.createNullProtoObjWherePossible(), escapeFn, include, rethrow]); }; if (opts.filename && typeof Object.defineProperty === 'function') { let filename = opts.filename; let basename = path.basename(filename, path.extname(filename)); try { Object.defineProperty(returnedFn, 'name', { value: basename, writable: false, enumerable: false, configurable: true }); } catch (e) {/* ignore */} } return returnedFn; }, generateSource: function () { let opts = this.opts; if (opts.rmWhitespace) { // Have to use two separate replace here as `^` and `$` operators don't // work well with `\r` and empty lines don't work well with the `m` flag. this.templateText = this.templateText.replace(/[\r\n]+/g, '\n').replace(/^\s+|\s+$/gm, ''); } let self = this; let d = this.opts.delimiter; let o = this.opts.openDelimiter; let c = this.opts.closeDelimiter; // Slurp spaces and tabs before opening whitespace slurp tag and after closing whitespace slurp tag // Build the tags using custom delimiters: openDelimiter + delimiter + '_' and '_' + delimiter + closeDelimiter let openWhitespaceSlurpTag = utils.escapeRegExpChars(o + d + '_'); let closeWhitespaceSlurpTag = utils.escapeRegExpChars('_' + d + c); let openWhitespaceSlurpReplacement = o + d + '_'; let closeWhitespaceSlurpReplacement = '_' + d + c; this.templateText = this.templateText.replace(new RegExp('[ \\t]*' + openWhitespaceSlurpTag, 'gm'), openWhitespaceSlurpReplacement) .replace(new RegExp(closeWhitespaceSlurpTag + '[ \\t]*', 'gm'), closeWhitespaceSlurpReplacement); let matches = this.parseTemplateText(); if (matches && matches.length) { matches.forEach(function (line, index) { let closing; // If this is an opening tag, check for closing tags // FIXME: May end up with some false positives here // Better to store modes as k/v with openDelimiter + delimiter as key // Then this can simply check against the map if ( line.indexOf(o + d) === 0 // If it is a tag && line.indexOf(o + d + d) !== 0) { // and is not escaped closing = matches[index + 2]; if (!(closing == d + c || closing == '-' + d + c || closing == '_' + d + c)) { throw new Error('Could not find matching close tag for "' + line + '".'); } } self.scanLine(line); }); } }, parseTemplateText: function () { let str = this.templateText; let pat = this.regex; let result = pat.exec(str); let arr = []; let firstPos; while (result) { firstPos = result.index; if (firstPos !== 0) { arr.push(str.substring(0, firstPos)); str = str.slice(firstPos); } arr.push(result[0]); str = str.slice(result[0].length); result = pat.exec(str); } if (str) { arr.push(str); } return arr; }, _addOutput: function (line) { if (this.truncate) { // Only replace single leading linebreak in the line after // -%> tag -- this is the single, trailing linebreak // after the tag that the truncation mode replaces // Handle Win / Unix / old Mac linebreaks -- do the \r\n // combo first in the regex-or line = line.replace(/^(?:\r\n|\r|\n)/, ''); this.truncate = false; } if (!line) { return line; } // Preserve literal slashes line = line.replace(/\\/g, '\\\\'); // Convert linebreaks line = line.replace(/\n/g, '\\n'); line = line.replace(/\r/g, '\\r'); // Escape double-quotes // - this will be the delimiter during execution line = line.replace(/"/g, '\\"'); this.source += ' ; __append("' + line + '")' + '\n'; }, scanLine: function (line) { let self = this; let d = this.opts.delimiter; let o = this.opts.openDelimiter; let c = this.opts.closeDelimiter; let newLineCount = 0; newLineCount = (line.split('\n').length - 1); switch (line) { case o + d: case o + d + '_': this.mode = Template.modes.EVAL; break; case o + d + '=': this.mode = Template.modes.ESCAPED; break; case o + d + '-': this.mode = Template.modes.RAW; break; case o + d + '#': this.mode = Template.modes.COMMENT; break; case o + d + d: this.mode = Template.modes.LITERAL; this.source += ' ; __append("' + line.replace(o + d + d, o + d) + '")' + '\n'; break; case d + d + c: this.mode = Template.modes.LITERAL; this.source += ' ; __append("' + line.replace(d + d + c, d + c) + '")' + '\n'; break; case d + c: case '-' + d + c: case '_' + d + c: if (this.mode == Template.modes.LITERAL) { this._addOutput(line); } this.mode = null; this.truncate = line.indexOf('-') === 0 || line.indexOf('_') === 0; break; default: // In script mode, depends on type of tag if (this.mode) { // If '//' is found without a line break, add a line break. switch (this.mode) { case Template.modes.EVAL: case Template.modes.ESCAPED: case Template.modes.RAW: if (line.lastIndexOf('//') > line.lastIndexOf('\n')) { line += '\n'; } } switch (this.mode) { // Just executing code case Template.modes.EVAL: this.source += ' ; ' + line + '\n'; break; // Exec, esc, and output case Template.modes.ESCAPED: this.source += ' ; __append(escapeFn(' + stripSemi(line) + '))' + '\n'; break; // Exec and output case Template.modes.RAW: this.source += ' ; __append(' + stripSemi(line) + ')' + '\n'; break; case Template.modes.COMMENT: // Do nothing break; // Literal <%% mode, append as raw output case Template.modes.LITERAL: this._addOutput(line); break; } } // In string mode, just add the output else { this._addOutput(line); } } if (self.opts.compileDebug && newLineCount) { this.currentLine += newLineCount; this.source += ' ; __line = ' + this.currentLine + '\n'; } } }; /** * Escape characters reserved in XML. * * This is simply an export of {@link module:utils.escapeXML}. * * If `markup` is `undefined` or `null`, the empty string is returned. * * @param {String} markup Input string * @return {String} Escaped string * @public * @func * */ ejs.escapeXML = utils.escapeXML; /** * Express.js support. * * This is an alias for {@link module:ejs.renderFile}, in order to support * Express.js out-of-the-box. * * @func */ ejs.__express = ejs.renderFile; /* istanbul ignore if */ if (typeof window != 'undefined') { window.ejs = ejs; } if (typeof module != 'undefined') { module.exports = ejs; } export default ejs; ================================================ FILE: lib/esm/package.json ================================================ { "type": "module" } ================================================ FILE: lib/esm/parseargs.js ================================================ /* * EJS Embedded JavaScript templates * Copyright 2112 Matthew Eernisse (mde@fleegix.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ 'use strict'; let parseargs = {}; let isOpt = function (arg) { return arg.indexOf('-') === 0; }; let removeOptPrefix = function (opt) { return opt.replace(/^--/, '').replace(/^-/, ''); }; /** * @constructor * Parses a list of command-line args into a key/value object of * options and an array of positional commands. * @param {Array} opts A list of options in the following format: * [{full: 'foo', abbr: 'f'}, {full: 'bar', abbr: 'b'}]] */ parseargs.Parser = function (opts) { // A key/value object of matching options parsed out of the args this.opts = {}; this.taskNames = null; this.envVars = null; // Data structures used for parsing this.reg = opts; this.shortOpts = {}; this.longOpts = {}; let self = this; [].forEach.call(opts, function (item) { self.shortOpts[item.abbr] = item; self.longOpts[item.full] = item; }); }; parseargs.Parser.prototype = new function () { let _trueOrNextVal = function (argParts, args) { if (argParts[1]) { return argParts[1]; } else { return (!args[0] || isOpt(args[0])) ? true : args.shift(); } }; /** * Parses an array of arguments into options and positional commands * @param {Array} args The command-line args to parse */ this.parse = function (args) { let cmds = []; let cmd; let envVars = {}; let opts = {}; let arg; let argItem; let argParts; let cmdItems; let taskNames = []; let preempt; while (args.length) { arg = args.shift(); if (isOpt(arg)) { arg = removeOptPrefix(arg); argParts = arg.split('='); argItem = this.longOpts[argParts[0]] || this.shortOpts[argParts[0]]; if (argItem) { // First-encountered preemptive opt takes precedence -- no further opts // or possibility of ambiguity, so just look for a value, or set to // true and then bail if (argItem.preempts) { opts[argItem.full] = _trueOrNextVal(argParts, args); preempt = true; break; } // If the opt requires a value, see if we can get a value from the // next arg, or infer true from no-arg -- if it's followed by another // opt, throw an error if (argItem.expectValue || argItem.allowValue) { opts[argItem.full] = _trueOrNextVal(argParts, args); if (argItem.expectValue && !opts[argItem.full]) { throw new Error(argItem.full + ' option expects a value.'); } } else { opts[argItem.full] = true; } } } else { cmds.unshift(arg); } } if (!preempt) { // Parse out any env-vars and task-name while ((cmd = cmds.pop())) { cmdItems = cmd.split('='); if (cmdItems.length > 1) { envVars[cmdItems[0]] = cmdItems[1]; } else { taskNames.push(cmd); } } } return { opts: opts, envVars: envVars, taskNames: taskNames }; }; }; if (typeof exports != 'undefined') { module.exports = parseargs; } export default parseargs; ================================================ FILE: lib/esm/utils.js ================================================ /* * EJS Embedded JavaScript templates * Copyright 2112 Matthew Eernisse (mde@fleegix.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * Private utility functions * @module utils * @private */ 'use strict'; const utils = {}; var regExpChars = /[|\\{}()[\]^$+*?.]/g; var hasOwnProperty = Object.prototype.hasOwnProperty; var hasOwn = function (obj, key) { return hasOwnProperty.apply(obj, [key]); }; /** * Escape characters reserved in regular expressions. * * If `string` is `undefined` or `null`, the empty string is returned. * * @param {String} string Input string * @return {String} Escaped string * @static * @private */ utils.escapeRegExpChars = function (string) { // istanbul ignore if if (!string) { return ''; } return String(string).replace(regExpChars, '\\$&'); }; var _ENCODE_HTML_RULES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; var _MATCH_HTML = /[&<>'"]/g; function encode_char(c) { return _ENCODE_HTML_RULES[c] || c; } /** * Stringified version of constants used by {@link module:utils.escapeXML}. * * @readonly * @type {String} */ var escapeFuncStr = 'var _ENCODE_HTML_RULES = {\n' + ' "&": "&"\n' + ' , "<": "<"\n' + ' , ">": ">"\n' + ' , \'"\': """\n' + ' , "\'": "'"\n' + ' }\n' + ' , _MATCH_HTML = /[&<>\'"]/g;\n' + 'function encode_char(c) {\n' + ' return _ENCODE_HTML_RULES[c] || c;\n' + '};\n'; /** * Escape characters reserved in XML. * * If `markup` is `undefined` or `null`, the empty string is returned. * * @implements {EscapeCallback} * @param {String} markup Input string * @return {String} Escaped string * @static * @private */ utils.escapeXML = function (markup) { return markup == undefined ? '' : String(markup) .replace(_MATCH_HTML, encode_char); }; function escapeXMLToString() { return Function.prototype.toString.call(this) + ';\n' + escapeFuncStr; } try { if (typeof Object.defineProperty === 'function') { // If the Function prototype is frozen, the "toString" property is non-writable. This means that any objects which inherit this property // cannot have the property changed using an assignment. If using strict mode, attempting that will cause an error. If not using strict // mode, attempting that will be silently ignored. // However, we can still explicitly shadow the prototype's "toString" property by defining a new "toString" property on this object. Object.defineProperty(utils.escapeXML, 'toString', { value: escapeXMLToString }); } else { // If Object.defineProperty() doesn't exist, attempt to shadow this property using the assignment operator. utils.escapeXML.toString = escapeXMLToString; } } catch (err) { console.warn('Unable to set escapeXML.toString (is the Function prototype frozen?)'); } /** * Naive copy of properties from one object to another. * Does not recurse into non-scalar properties * Does not check to see if the property has a value before copying * * @param {Object} to Destination object * @param {Object} from Source object * @return {Object} Destination object * @static * @private */ utils.shallowCopy = function (to, from) { from = from || {}; if ((to !== null) && (to !== undefined)) { for (var p in from) { if (!hasOwn(from, p)) { continue; } if (p === '__proto__' || p === 'constructor') { continue; } to[p] = from[p]; } } return to; }; /** * Naive copy of a list of key names, from one object to another. * Only copies property if it is actually defined * Does not recurse into non-scalar properties * * @param {Object} to Destination object * @param {Object} from Source object * @param {Array} list List of properties to copy * @return {Object} Destination object * @static * @private */ utils.shallowCopyFromList = function (to, from, list) { list = list || []; from = from || {}; if ((to !== null) && (to !== undefined)) { for (var i = 0; i < list.length; i++) { var p = list[i]; if (typeof from[p] != 'undefined') { if (!hasOwn(from, p)) { continue; } if (p === '__proto__' || p === 'constructor') { continue; } to[p] = from[p]; } } } return to; }; /** * Simple in-process cache implementation. Does not implement limits of any * sort. * * @implements {Cache} * @static * @private */ utils.cache = { _data: {}, set: function (key, val) { this._data[key] = val; }, get: function (key) { return this._data[key]; }, remove: function (key) { delete this._data[key]; }, reset: function () { this._data = {}; } }; /** * Transforms hyphen case variable into camel case. * * @param {String} string Hyphen case string * @return {String} Camel case string * @static * @private */ utils.hyphenToCamel = function (str) { return str.replace(/-[a-z]/g, function (match) { return match[1].toUpperCase(); }); }; /** * Returns a null-prototype object in runtimes that support it * * @return {Object} Object, prototype will be set to null where possible * @static * @private */ utils.createNullProtoObjWherePossible = (function () { if (typeof Object.create == 'function') { return function () { return Object.create(null); }; } if (!({__proto__: null} instanceof Object)) { return function () { return {__proto__: null}; }; } // Not possible, just pass through return function () { return {}; }; })(); /** * Copies own-properties from one object to a null-prototype object for basic * protection against prototype pollution * * @return {Object} Object with own-properties of input object * @static * @private */ utils.hasOwnOnlyObject = function (obj) { var o = utils.createNullProtoObjWherePossible(); for (var p in obj) { if (hasOwn(obj, p)) { o[p] = obj[p]; } } return o; }; if (typeof exports != 'undefined') { module.exports = utils; } export default utils; ================================================ FILE: package.json ================================================ { "name": "ejs", "description": "Embedded JavaScript templates", "keywords": [ "template", "templating", "engine", "ejs" ], "version": "5.0.1", "author": "Matthew Eernisse ", "license": "Apache-2.0", "bin": { "ejs": "./bin/cli.js" }, "main": "./lib/cjs/ejs.js", "module": "./lib/esm/ejs.js", "browser": "./ejs.min.js", "exports": { "import": "./lib/esm/ejs.js", "require": "./lib/cjs/ejs.js" }, "jsdelivr": "ejs.min.js", "unpkg": "ejs.min.js", "repository": { "type": "git", "url": "git://github.com/mde/ejs.git" }, "bugs": "https://github.com/mde/ejs/issues", "homepage": "https://github.com/mde/ejs", "devDependencies": { "jake": "^10.9.1", "typescript": "^5.4.5", "@babel/eslint-parser": "^7.24.5", "browserify": "^17.0.0", "eslint": "^9.1.1", "git-directory-deploy": "^1.5.1", "jsdoc": "^4.0.2", "lru-cache": "^4.0.1", "mocha": "^10.2.0", "uglify-js": "^3.3.16" }, "engines": { "node": ">=0.12.18" }, "scripts": { "test": "npx jake test" } } ================================================ FILE: test/cli.js ================================================ let exec = require('child_process').execSync; let fs = require('fs'); let path = require('path'); let assert = require('assert'); let os = process.platform !== 'win32' ? '' : 'node '; let lf = process.platform !== 'win32' ? '\n' : '\r\n'; function run(cmd) { return exec(cmd).toString(); } suite('cli', function () { test('rendering, custom delimiter, passed data', function () { let x = path.join('./bin/cli.js'); let u = path.join('./test/fixtures/user.ejs'); let o = run(os+x+' -m $ '+u+' name=foo'); assert.equal(o, '

    foo

    '+lf); }); test('rendering, custom delimiter, data from file with -f', function () { let x = path.join('./bin/cli.js'); let u = path.join('./test/fixtures/user.ejs'); let o = run(os+x+' -m $ -f ./test/fixtures/user_data.json '+u); assert.equal(o, '

    zerb

    '+lf); }); test('rendering, custom delimiter, data from CLI arg with -i', function () { let x = path.join('./bin/cli.js'); let u = path.join('./test/fixtures/user.ejs'); let o = run(os+x+' -m $ -i %7B%22name%22%3A%20%22foo%22%7D '+u); assert.equal(o, '

    foo

    '+lf); }); test('rendering, custom delimiter, data from stdin / pipe', function () { if ( process.platform !== 'win32' ) { let o = run('cat ./test/fixtures/user_data.json | ./bin/cli.js -m $ ./test/fixtures/user.ejs'); assert.equal(o, '

    zerb

    \n'); } // does not work on windows... }); test('rendering, custom delimiter, passed data overrides file', function () { let x = path.join('./bin/cli.js'); let f = path.join('./test/fixtures/user_data.json'); let g = path.join('./test/fixtures/user.ejs'); let o = run(os+x+' -m $ -f '+f+' '+g+' name=frang'); assert.equal(o, '

    frang

    '+lf); }); test('rendering, remove whitespace option (hyphen case)', function () { let x = path.join('./bin/cli.js'); let f = path.join('./test/fixtures/rmWhitespace.ejs'); let o = run(os+x+' --rm-whitespace '+f); let c = fs.readFileSync('test/fixtures/rmWhitespace.html', 'utf-8'); assert.equal(o.replace(/\n/g, lf), c); }); test('relative path in nested include', function () { let x = path.join('./bin/cli.js'); let u = path.join('test/fixtures/include-simple.ejs'); let o = run(os+x+' '+u); let c = fs.readFileSync('test/fixtures/include-simple.html', 'utf-8'); assert.equal(o, c); }); }); ================================================ FILE: test/ejs.js ================================================ /* jshint mocha: true */ /* eslint-env node, mocha */ /** * Module dependencies. */ var ejs = require('..'); var fs = require('fs'); var read = fs.readFileSync; var assert = require('assert'); var path = require('path'); var LRU = require('lru-cache'); let lf = process.platform !== 'win32' ? '\n' : '\r\n'; try { fs.mkdirSync(__dirname + '/tmp'); } catch (ex) { if (ex.code !== 'EEXIST') { throw ex; } } // From https://gist.github.com/pguillory/729616 function hook_stdio(stream, callback) { var old_write = stream.write; stream.write = (function() { return function(string, encoding, fd) { callback(string, encoding, fd); }; })(stream.write); return function() { stream.write = old_write; }; } /** * Load fixture `name`. */ function fixture(name) { return read('test/fixtures/' + name, 'utf8'); } /** * User fixtures. */ var users = []; users.push({name: 'geddy'}); users.push({name: 'neil'}); users.push({name: 'alex'}); /** Used to test code that depends on async functions being supported. */ var testAsync = test.skip; try { eval('(async function() {})'); testAsync = test; } catch (e) { // ignore } suite('ejs.compile(str, options)', function () { test('compile to a function', function () { var fn = ejs.compile('

    yay

    '); assert.equal(fn(), '

    yay

    '); }); test('empty input works', function () { var fn = ejs.compile(''); assert.equal(fn(), ''); }); test('throw if there are syntax errors', function () { try { ejs.compile(fixture('fail.ejs')); } catch (err) { assert.ok(err.message.indexOf('compiling ejs') > -1); try { ejs.compile(fixture('fail.ejs'), {filename: 'fail.ejs'}); } catch (err) { assert.ok(err.message.indexOf('fail.ejs') > -1); return; } } throw new Error('no error reported when there should be'); }); test('allow customizing delimiter local var', function () { var fn; fn = ejs.compile('

    ', {delimiter: '?'}); assert.equal(fn({name: 'geddy'}), '

    geddy

    '); fn = ejs.compile('

    <:= name :>

    ', {delimiter: ':'}); assert.equal(fn({name: 'geddy'}), '

    geddy

    '); fn = ejs.compile('

    <$= name $>

    ', {delimiter: '$'}); assert.equal(fn({name: 'geddy'}), '

    geddy

    '); }); test('allow customizing open and close delimiters', function() { var fn; fn = ejs.compile('

    [#= name #]

    ', {delimiter: '#', openDelimiter: '[', closeDelimiter: ']'}); assert.equal(fn({name: 'geddy'}), '

    geddy

    '); }); test('default to using ejs.delimiter', function () { var fn; ejs.delimiter = '&'; fn = ejs.compile('

    <&= name &>

    '); assert.equal(fn({name: 'geddy'}), '

    geddy

    '); fn = ejs.compile('

    <|= name |>

    ', {delimiter: '|'}); assert.equal(fn({name: 'geddy'}), '

    geddy

    '); delete ejs.delimiter; }); test('support custom escape function', function () { var customEscape; var fn; customEscape = function customEscape(str) { return !str ? '' : str.toUpperCase(); }; fn = ejs.compile('HELLO <%= name %>', {escape: customEscape}); assert.equal(fn({name: 'world'}), 'HELLO WORLD'); }); test('strict mode works', function () { assert.equal(ejs.render(fixture('strict.ejs'), {}, {strict: true}), 'true'); }); test('destructuring works in strict mode as an alternative to `with`', function () { var locals = Object.create(null); locals.foo = 'bar'; assert.equal(ejs.render(fixture('strict-destructuring.ejs'), locals, { strict: true, destructuredLocals: Object.keys(locals), _with: true }), locals.foo); }); testAsync('destructuring works in strict and async mode', function (done) { var locals = Object.create(null); locals.foo = 'bar'; ejs.render(fixture('strict-destructuring.ejs'), locals, { strict: true, async: true, destructuredLocals: Object.keys(locals), }).then(function (value) { assert.equal(value, locals.foo); }).then( () => done(), e => done(e) ); }); testAsync('can compile to an async function', function (done) { ejs.compile('<%= await "Hi" %>', {async: true})().then(function (value) { try { assert.equal(value, 'Hi'); } catch (e) { done(e); return; } done(); }); }); testAsync('Non-async error message mentions `async: true`', function (done) { try { ejs.compile('<%= await "Hi" %>'); } catch (err) { if (err instanceof SyntaxError) { assert.ok(err.message.indexOf('async: true') > -1); return done(); } else { throw err; } } throw new Error('no error reported when there should be'); }); var testFuncName = typeof Object.defineProperty === 'function' ? test : test.skip; testFuncName('Compiled function name matches `filename` without the extension', function (done) { var func = ejs.compile('<%= "Foo" %>', { filename: 'foo.ejs' }); assert.ok(func.name === 'foo'); return done(); }); testFuncName('Compiled function name defaults to "anonymous" when `filename` is unspecified', function (done) { var func = ejs.compile('<%= "Foo" %>'); assert.ok(func.name === 'anonymous'); return done(); }); }); /* Old API -- remove when this shim goes away */ suite('ejs.render(str, dataAndOpts)', function () { test('render the template with data/opts passed together', function () { assert.equal(ejs.render('

    ', {foo: 'yay', delimiter: '?'}), '

    yay

    '); }); test('disallow unsafe opts passed along in data', function () { assert.equal(ejs.render('

    ', // localsName should not get reset because it's blacklisted {_with: false, foo: 'yay', delimiter: '?', localsName: '_'}), '

    yay

    '); }); }); suite('ejs.render(str, data, opts)', function () { test('render the template', function () { assert.equal(ejs.render('

    yay

    '), '

    yay

    '); }); test('empty input works', function () { assert.equal(ejs.render(''), ''); }); test('undefined renders nothing escaped', function () { assert.equal(ejs.render('<%= undefined %>'), ''); }); test('undefined renders nothing raw', function () { assert.equal(ejs.render('<%- undefined %>'), ''); }); test('null renders nothing escaped', function () { assert.equal(ejs.render('<%= null %>'), ''); }); test('null renders nothing raw', function () { assert.equal(ejs.render('<%- null %>'), ''); }); test('zero-value data item renders something escaped', function () { assert.equal(ejs.render('<%= 0 %>'), '0'); }); test('zero-value data object renders something raw', function () { assert.equal(ejs.render('<%- 0 %>'), '0'); }); test('accept locals', function () { assert.equal(ejs.render('

    <%= name %>

    ', {name: 'geddy'}), '

    geddy

    '); }); test('accept locals without using with() {}', function () { assert.equal(ejs.render('

    <%= locals.name %>

    ', {name: 'geddy'}, {_with: false}), '

    geddy

    '); assert.throws(function() { ejs.render('

    <%= name %>

    ', {name: 'geddy'}, {_with: false}); }, /name is not defined/); }); test('accept custom name for locals', function () { ejs.localsName = 'it'; assert.equal(ejs.render('

    <%= it.name %>

    ', {name: 'geddy'}, {_with: false}), '

    geddy

    '); assert.throws(function() { ejs.render('

    <%= name %>

    ', {name: 'geddy'}, {_with: false}); }, /name is not defined/); ejs.localsName = 'locals'; }); test('support caching', function () { var file = __dirname + '/tmp/render.ejs'; var options = {cache: true, filename: file}; var out = ejs.render('

    Old

    ', {}, options); var expected = '

    Old

    '; assert.equal(out, expected); // Assert no change, still in cache out = ejs.render('

    New

    ', {}, options); assert.equal(out, expected); }); test('support LRU caching', function () { var oldCache = ejs.cache; var file = __dirname + '/tmp/render.ejs'; var options = {cache: true, filename: file}; var out; var expected = '

    Old

    '; // Switch to LRU ejs.cache = LRU(); out = ejs.render('

    Old

    ', {}, options); assert.equal(out, expected); // Assert no change, still in cache out = ejs.render('

    New

    ', {}, options); assert.equal(out, expected); // Restore system cache ejs.cache = oldCache; }); test('opts.context', function () { var ctxt = {foo: 'FOO'}; var out = ejs.render('<%= this.foo %>', {}, {context: ctxt}); assert.equal(out, ctxt.foo); }); }); suite('ejs.renderFile(path, [data], [options], [fn])', function () { test('render a file', function(done) { ejs.renderFile('test/fixtures/para.ejs', function(err, html) { if (err) { return done(err); } assert.equal(html, '

    hey

    '+lf); done(); }); }); test('Promise support for reading files', function(done) { var AsyncCtor; var func; function checkResult(html) { assert.equal(html, '

    hey

    '+lf); } // Environments without Promise support -- should throw // when no callback provided function checkNoPromise() { delete ejs.promiseImpl; assert.throws(function () { ejs.renderFile('test/fixtures/para.ejs'); }); ejs.promiseImpl = global.Promise; done(); } // Check for async/await support -- have to use eval for check because // envs without async will break at parsing step try { eval('AsyncCtor = (async function () {}).constructor;'); } catch (e) { // No-op } // Both async and Promise -- in both cases, also check the call // correctly throws in non-Promise envs if no callback provided // ------------------- // Async support -- have to use eval for constructing async func for // same reasons as above if (AsyncCtor) { func = new AsyncCtor('ejs', 'checkResult', 'checkNoPromise', "let res = await ejs.renderFile('test/fixtures/para.ejs'); checkResult(res); checkNoPromise();"); func(ejs, checkResult, checkNoPromise); } // Ordinary Promise support else { ejs.renderFile('test/fixtures/para.ejs').then(function (res) { checkResult(res); checkNoPromise(); }); } }); test('accept locals', function(done) { var data = {name: 'fonebone'}; var options = {delimiter: '$'}; ejs.renderFile('test/fixtures/user.ejs', data, options, function(err, html) { if (err) { return done(err); } assert.equal(html, '

    fonebone

    '+lf); done(); }); }); test('accept locals without using with() {}', function(done) { var data = {name: 'fonebone'}; var options = {delimiter: '$', _with: false}; var doneCount = 0; ejs.renderFile('test/fixtures/user-no-with.ejs', data, options, function(err, html) { if (err) { if (doneCount === 2) { return; } doneCount = 2; return done(err); } assert.equal(html, '

    fonebone

    '+lf); doneCount++; if (doneCount === 2) { done(); } }); ejs.renderFile('test/fixtures/user.ejs', data, options, function(err) { if (!err) { if (doneCount === 2) { return; } doneCount = 2; return done(new Error('error not thrown')); } doneCount++; if (doneCount === 2) { done(); } }); }); test('not catch err thrown by callback', function(done) { var data = {name: 'fonebone'}; var options = {delimiter: '$'}; var counter = 0; var d = require('domain').create(); d.on('error', function (err) { assert.equal(counter, 1); assert.equal(err.message, 'Exception in callback'); done(); }); d.run(function () { // process.nextTick() needed to work around mochajs/mocha#513 // // tl;dr: mocha doesn't support synchronous exception throwing in // domains. Have to make it async. Ticket closed because: "domains are // deprecated :D" process.nextTick(function () { ejs.renderFile('test/fixtures/user.ejs', data, options, function(err) { counter++; if (err) { assert.notEqual(err.message, 'Exception in callback'); return done(err); } throw new Error('Exception in callback'); }); }); }); }); test('support caching', function (done) { var expected = '

    Old

    '; var file = __dirname + '/tmp/renderFile.ejs'; var options = {cache: true}; fs.writeFileSync(file, '

    Old

    '); ejs.renderFile(file, {}, options, function (err, out) { if (err) { done(err); } fs.writeFileSync(file, '

    New

    '); assert.equal(out, expected); ejs.renderFile(file, {}, options, function (err, out) { if (err) { done(err); } // Assert no change, still in cache assert.equal(out, expected); done(); }); }); }); test('opts.context', function (done) { var ctxt = {foo: 'FOO'}; ejs.renderFile('test/fixtures/with-context.ejs', {}, {context: ctxt}, function(err, html) { if (err) { return done(err); } assert.equal(html, ctxt.foo + lf); done(); }); }); test('support express multiple views folders, falls back to second if first is not available', function (done) { var data = { viewsText: 'test', includePath: 'views-include.ejs', settings: { views: [ path.join(__dirname, 'fixtures/nonexistent-folder'), path.join(__dirname, 'fixtures') ] } }; ejs.renderFile(path.join(__dirname, 'fixtures/views.ejs'), data, function(error, data){ assert.ifError(error); assert.equal('

    global test

    '+lf+'
    '+lf, data); done(); }); }); test('can reference by paths with directory names', function (done) { var data = { viewsText: 'test', includePath: 'views/views-include.ejs', settings: { views: [ path.join(__dirname, 'fixtures/views'), path.join(__dirname, 'fixtures') ] } }; ejs.renderFile(path.join(__dirname, 'fixtures/views.ejs'), data, function(error, data){ assert.ifError(error); assert.equal('

    custom test

    '+lf+'
    '+lf, data); done(); }); }); }); suite('cache specific', function () { test('`clearCache` work properly', function () { var expected = '

    Old

    '; var file = __dirname + '/tmp/clearCache.ejs'; var options = {cache: true, filename: file}; var out = ejs.render('

    Old

    ', {}, options); assert.equal(out, expected); ejs.clearCache(); expected = '

    New

    '; out = ejs.render('

    New

    ', {}, options); assert.equal(out, expected); }); test('`clearCache` work properly, LRU', function () { var expected = '

    Old

    '; var oldCache = ejs.cache; var file = __dirname + '/tmp/clearCache.ejs'; var options = {cache: true, filename: file}; var out; ejs.cache = LRU(); out = ejs.render('

    Old

    ', {}, options); assert.equal(out, expected); ejs.clearCache(); expected = '

    New

    '; out = ejs.render('

    New

    ', {}, options); assert.equal(out, expected); ejs.cache = oldCache; }); test('LRU with cache-size 1', function () { var oldCache = ejs.cache; var options; var out; var expected; var file; ejs.cache = LRU(1); file = __dirname + '/tmp/render1.ejs'; options = {cache: true, filename: file}; out = ejs.render('

    File1

    ', {}, options); expected = '

    File1

    '; assert.equal(out, expected); // Same filename, different template, but output // should be the same because cache file = __dirname + '/tmp/render1.ejs'; options = {cache: true, filename: file}; out = ejs.render('

    ChangedFile1

    ', {}, options); expected = '

    File1

    '; assert.equal(out, expected); // Different filiename -- output should be different, // and previous cache-entry should be evicted file = __dirname + '/tmp/render2.ejs'; options = {cache: true, filename: file}; out = ejs.render('

    File2

    ', {}, options); expected = '

    File2

    '; assert.equal(out, expected); // Entry with first filename should now be out of cache, // results should be different file = __dirname + '/tmp/render1.ejs'; options = {cache: true, filename: file}; out = ejs.render('

    ChangedFile1

    ', {}, options); expected = '

    ChangedFile1

    '; assert.equal(out, expected); ejs.cache = oldCache; }); }); suite('<%', function () { test('without semicolons', function () { assert.equal(ejs.render(fixture('no.semicolons.ejs')), fixture('no.semicolons.html')); }); }); suite('<%=', function () { test('should not throw an error with a // comment on the final line', function () { assert.equal(ejs.render('<%=\n// a comment\nname\n// another comment %>', {name: '