nunjucks-fd500902d7c88672470c87170796de52fc0f791a/000775 000000 000000 00000000000 14012546311 020053 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.babelrc000664 000000 000000 00000000552 14012546311 021450 0ustar00rootroot000000 000000 { "presets": [ ["@babel/env", { "loose": true, "targets": { "browsers": ["last 2 versions", "safari >= 7", "ie 9"], "node": "6" } }] ], "env": { "test": { "plugins": ["./scripts/lib/arrow-function-coverage-fix", "istanbul"] } }, "ignore": [ "scripts/lib/arrow-function-coverage-fix.js" ] } nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.eslintignore000664 000000 000000 00000000172 14012546311 022556 0ustar00rootroot000000 000000 node_modules spm_modules coverage dist browser docs tests/express-sample tests/express tests/browser bench src/loaders.js nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.eslintrc.js000664 000000 000000 00000003215 14012546311 022313 0ustar00rootroot000000 000000 module.exports = { 'extends': [ 'airbnb-base/legacy', ], 'parserOptions': { 'sourceType': 'module', 'ecmaVersion': 2017, }, 'env': { 'node': true, 'es6': true, }, "rules": { // The one assertion of personal preference: no spaces before parentheses // of anonymous functions 'space-before-function-paren': ['error', { anonymous: 'never', named: 'never', asyncArrow: 'always', }], // Temporarily disabled rules // // no-use-before-define is a good rule, but it would make the diff for // linting the code even more inscrutible than it already is. 'no-use-before-define': 'off', // Relax some rules 'no-cond-assign': ['error', 'except-parens'], 'no-unused-vars': ['error', { 'args': 'none', }], // Disable some overly-strict airbnb style rules 'no-underscore-dangle': 'off', 'no-param-reassign': 'off', 'class-methods-use-this': 'off', 'function-paren-newline': 'off', 'no-plusplus': 'off', 'object-curly-spacing': 'off', 'no-multi-assign': 'off', 'no-else-return': 'off', // While technically useless from the point of view of the regex parser, // escaping characters inside character classes is more consistent. I // would say that they make the regular expression more readable, if the // idea of readable regular expressions wasn't absurd on its face. 'no-useless-escape': 'off', // I'm inclined to reverse this rule to be ['error', 'always'], but not just yet // IE 8 is a thing of the past and trailing commas are useful. 'comma-dangle': 'off', }, 'globals': { 'nunjucks': false, }, }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.gitattributes000664 000000 000000 00000000305 14012546311 022744 0ustar00rootroot000000 000000 # Set the default behavior, in case people don't have core.autocrlf set. * text=auto # Checkout example and test fixtures with linux line ending # to guarantee test successes tests/** text eol=lf nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.github/000775 000000 000000 00000000000 14012546311 021413 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.github/PULL_REQUEST_TEMPLATE.md000664 000000 000000 00000002046 14012546311 025216 0ustar00rootroot000000 000000 ## Summary Proposed change: Closes # . ## Checklist I've completed the checklist below to ensure I didn't forget anything. This makes reviewing this PR as easy as possible for the maintainers. And it gets this change released as soon as possible. * [ ] Proposed change helps towards [*purpose of this project*](https://github.com/mozilla/nunjucks/blob/master/CONTRIBUTING.md#purpose). * [ ] [*Documentation*](https://github.com/mozilla/nunjucks/tree/master/docs/) is added / updated to describe proposed change. * [ ] [*Tests*](https://github.com/mozilla/nunjucks/tree/master/tests) are added / updated to cover proposed change. * [ ] [*Changelog*](https://github.com/mozilla/nunjucks/blob/master/CHANGELOG.md) has an entry for proposed change (if user-facing fix or feature). nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.github/workflows/000775 000000 000000 00000000000 14012546311 023450 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.github/workflows/tests.yml000664 000000 000000 00000002025 14012546311 025334 0ustar00rootroot000000 000000 name: Tests on: push: branches: - '**' pull_request: branches: - master paths: - '**/*.js' - 'package.json' - '**/*.njk' - '**/*.yml' - 'tests/**/*.html' jobs: tests: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] node-version: [11.x, 10.x, 8.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - uses: actions/cache@v2 id: cache-node-modules with: path: node_modules key: ${{ matrix.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} - run: npm install if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: npm test # Commented out until this action is added in organization settings # - name: Report coverage # if: success() # uses: codecov/codecov-action@v1 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.gitignore000664 000000 000000 00000000345 14012546311 022045 0ustar00rootroot000000 000000 node_modules coverage .#* docs/_site docs/files /src/ /index.js /index.js.map /tests/browser/nunjucks* .nyc_output /browser /tests/browser/precompiled-templates.js /samples/express/js/nunjucks.js /samples/express/js/templates.js nunjucks-fd500902d7c88672470c87170796de52fc0f791a/.npmignore000664 000000 000000 00000000104 14012546311 022045 0ustar00rootroot000000 000000 node_modules coverage .nyc_output docs tests bench nunjucks scripts nunjucks-fd500902d7c88672470c87170796de52fc0f791a/CHANGELOG.md000664 000000 000000 00000072027 14012546311 021674 0ustar00rootroot000000 000000 Changelog ========= 3.2.3 (Feb 15 2021) ------------------- * Add support for nested attributes on [`sort` filter](https://mozilla.github.io/nunjucks/templating.html#sort-arr-reverse-casesens-attr); respect `throwOnUndefined` if sort attribute is undefined. * Add `base` arg to [`int` filter](https://mozilla.github.io/nunjucks/templating.html#int). * Move `chokidar` to `peerDependencies` and mark it `optional` in `peerDependenciesMeta`. * Fix prototype pollution issue for template variables. Merge of [#1330](https://github.com/mozilla/nunjucks/pull/1330); fixes [#1331](https://github.com/mozilla/nunjucks/issues/1331). Thanks [ChenKS12138](https://github.com/ChenKS12138)! 3.2.2 (Jul 20 2020) ------------------- * Add [`select`](https://mozilla.github.io/nunjucks/templating.html#select) and [`reject`](https://mozilla.github.io/nunjucks/templating.html#reject) filters. Merge of [#1278](https://github.com/mozilla/nunjucks/pull/1278) and [#1279](https://github.com/mozilla/nunjucks/pull/1279); fixes [#282](https://github.com/mozilla/nunjucks/issues/282). Thanks [ogonkov](https://github.com/ogonkovv)! * Fix precompile binary script `TypeError: name.replace is not a function`. Fixes [#1295](https://github.com/mozilla/nunjucks/issues/1295). * Add support for nested attributes on [`groupby` filter](https://mozilla.github.io/nunjucks/templating.html#groupby); respect `throwOnUndefined` option, if the groupby attribute is undefined. Merge of [#1276](https://github.com/mozilla/nunjucks/pull/1276); fixes [#1198](https://github.com/mozilla/nunjucks/issues/1198). Thanks [ogonkov](https://github.com/ogonkovv)! * Fix bug that prevented errors in included templates from being raised when rendering templates synchronously. Fixes [#1272](https://github.com/mozilla/nunjucks/issues/1272). * The `indent` filter no longer appends an additional newline. Fixes [#1231](https://github.com/mozilla/nunjucks/issues/1231). 3.2.1 (Mar 17 2020) ------------------- * Replace yargs with commander to reduce number of dependencies. Merge of [#1253](https://github.com/mozilla/nunjucks/pull/1253). Thanks [AlynxZhou](@AlynxZhou). * Update optional dependency chokidar from `^2.0.0` to `^3.3.0`. Merge of [#1254](https://github.com/mozilla/nunjucks/pull/1254). Thanks [eklingen](@eklingen). * Prevent optional dependency Chokidar from loading when not watching. Merge of [#1250](https://github.com/mozilla/nunjucks/pull/1250). Thanks [eklingen](@eklingen). 3.2.0 (Mar 5 2019) ------------------ * Adds [`NodeResolveLoader`](http://mozilla.github.io/nunjucks/api.html#noderesolveloader), a Loader that loads templates using node's [`require.resolve`](https://nodejs.org/api/modules.html#modules_all_together). Fixes [#1175](https://github.com/mozilla/nunjucks/issues/1175). * Emit 'load' events on `Environment` instances, to allow runtime dependency tracking. Fixes [#1153](https://github.com/mozilla/nunjucks/issues/1153). 3.1.7 (Jan 12 2019) ------------------ * Fix bug where exceptions were silently swallowed with synchronous render. Fixes [#678](https://github.com/mozilla/nunjucks/issues/678), [#1116](https://github.com/mozilla/nunjucks/issues/1116), [#1127](https://github.com/mozilla/nunjucks/issues/1127), and [#1164](https://github.com/mozilla/nunjucks/issues/1164) * Removes deprecated postinstall-build package in favor of [npm prepare](https://docs.npmjs.com/misc/scripts#prepublish-and-prepare). Merge of [#1172](https://github.com/mozilla/nunjucks/pull/1172). Fixes [#1167](https://github.com/mozilla/nunjucks/issues/1167). - Note: this means that npm@5 or later is required to install nunjucks directly from github. 3.1.6 (Dec 13 2018) ------------------- No code changes; fixed npm packaging issue. 3.1.5 (Dec 13 2018) ------------------- * Fix engine dependency version for Node versions > 11.1.0; Fixes [#1168](https://github.com/mozilla/nunjucks/issues/1168). 3.1.4 (Nov 9 2018) ------------------ * Fix engine version for Node v11.1.0 * Fix "Unexpected token" error for U+2028 unicode newline. Fixes [#126](https://github.com/mozilla/nunjucks/issues/126) and [#736](https://github.com/mozilla/nunjucks/issues/736) 3.1.3 (May 19 2018) ------------------- * Add `forceescape` filter. Fixes [#782](https://github.com/mozilla/nunjucks/issues/782) * Fix regression that prevented template errors from reporting line and column number. Fixes [#1087](https://github.com/mozilla/nunjucks/issues/1087) and [#1095](https://github.com/mozilla/nunjucks/issues/1095). * Fix "Invalid type: Is" error for `{% if value is defined %}`. Fixes [#1110](https://github.com/mozilla/nunjucks/issues/1110) * Formally drop support for node v4 (the upgrade to babel 7 in 3.1.0 made the build process incompatible with node < 6.9.0). 3.1.2 (Feb 23 2018) ------------------- * Fix regression to make `chokidar` an optional dependency again. Fixes [#1073](https://github.com/mozilla/nunjucks/issues/1073) * Fix issue when running `npm install nunjucks` with the `--no-bin-links` flag * Fix regression that broke template caching. Fixes [#1074](https://github.com/mozilla/nunjucks/issues/1074) 3.1.0 (Feb 19 2018) ------------------- * Support nunjucks.installJinjaCompat() with slim build. Fixes [#1019](https://github.com/mozilla/nunjucks/issues/1019) * Fix calling render callback twice when a conditional import throws an error. Solves [#1029](https://github.com/mozilla/nunjucks/issues/1029) * Support objects created with Object.create(null). fixes [#468](https://github.com/mozilla/nunjucks/issues/468) * Support ESNext iterators, using Array.from. Merge of [#1058](https://github.com/mozilla/nunjucks/pull/1058) 3.0.1 (May 24 2017) ------------------- * Fix handling methods and attributes of static arrays, objects and primitives. Solves the issue [#937](https://github.com/mozilla/nunjucks/issues/937) * Add support for python-style array slices with Jinja compat enabled. Fixes [#188](https://github.com/mozilla/nunjucks/issues/188); merge of [#976](https://github.com/mozilla/nunjucks/pull/976). * Fix call blocks having access to their parent scope. Fixes [#906](https://github.com/mozilla/nunjucks/issues/906); merge of [#994](https://github.com/mozilla/nunjucks/pull/994). * Fix a bug that caused capturing block tags (e.g. set/endset, filter/endfilter) to write to the global buffer rather than capturing their contents. Fixes [#914](https://github.com/mozilla/nunjucks/issues/914) and [#972](https://github.com/mozilla/nunjucks/issues/972); merge of [#990](https://github.com/mozilla/nunjucks/pull/990). Thanks [Noah Lange](@noahlange). 3.0.0 (Nov 5 2016) ---------------- * Allow including many templates without reaching recursion limits. Merge of [#787](https://github.com/mozilla/nunjucks/pull/787). Thanks Gleb Khudyakov. * Allow explicitly setting `null` (aka `none`) as the value of a variable; don't ignore that value and look on up the frame stack or context. Fixes [#478](https://github.com/mozilla/nunjucks/issues/478). Thanks Jonny Gerig Meyer for the report. * Execute blocks in a child frame that can't write to its parent. This means that vars set inside blocks will not leak outside of the block, base templates can no longer see vars set in templates that inherit them, and `super()` can no longer set vars in its calling scope. Fixes the inheritance portion of [#561](https://github.com/mozilla/nunjucks/issues/561), which fully closes that issue. Thanks legutierr for the report. * Prevent macros from seeing or affecting their calling scope. Merge of [#667](https://github.com/mozilla/nunjucks/pull/667). * Fix handling of macro arg with default value which shares a name with another macro. Merge of [#791](https://github.com/mozilla/nunjucks/pull/791). * Add support for the spaces parameter in the dump template filter. Merge of [#868](https://github.com/mozilla/nunjucks/pull/868). Thanks Jesse Eikema * Add `verbatim` as an alias of `raw` for compatibility with Twig. Merge of [#874](https://github.com/mozilla/nunjucks/pull/874). * Add new `nl2br` filter. Thanks Marc-Aurèle Darche * Add support for python's `list.append` with Jinja compat enabled. Thanks Conor Flannigan. * Add variables whitespace control. 2.5.2 (Sep 14 2016) ---------------- * Call `.toString` in safe filter. Merge of [#849](https://github.com/mozilla/nunjucks/pull/849). 2.5.1 (Sep 13 2016) ---------------- * Fix `undefined` and `null` behavior in escape and safe filter. Merge of [#843](https://github.com/mozilla/nunjucks/pull/843). 2.5.0 (Sep 7 2016) ---------------- * Add `elseif` as an alias of `elif` for parity with Twig. Thanks kswedberg. Merge of [#826](https://github.com/mozilla/nunjucks/pull/826). * Add nunjucks env to express app settings as `nunjucksEnv`. Merge of [#829](https://github.com/mozilla/nunjucks/pull/829). * Add support for finding an object's "length" in length filter. Merge of [#813](https://github.com/mozilla/nunjucks/pull/813). * Ensure that precompiling on Windows still outputs POSIX-style path separators. Merge of [#761](https://github.com/mozilla/nunjucks/pull/761). * Add support for strict type check comparisons (=== and !==). Thanks oughter. Merge of [#746](https://github.com/mozilla/nunjucks/pull/746). * Allow full expressions (incl. filters) in import and from tags. Thanks legutierr. Merge of [#710](https://github.com/mozilla/nunjucks/pull/710). * OS agnostic file paths in precompile. Merge of [#825](https://github.com/mozilla/nunjucks/pull/825). 2.4.3 (Sep 7 2016) ---------------- * Fix potential cast-related XSS vulnerability in autoescape mode, and with `escape` filter. Thanks Matt Austin for the report and Thomas Hunkapiller for the fix. [#836](https://github.com/mozilla/nunjucks/pull/836) 2.4.2 (Apr 15 2016) ------------------- * Fix use of `in` operator with strings. Fixes [#714](https://github.com/mozilla/nunjucks/issues/714). Thanks Zubrik for the report. * Support ES2015 Map and Set in `length` filter. Merge of [#705](https://github.com/mozilla/nunjucks/pull/705). Thanks ricordisamoa. * Remove truncation of long function names in error messages. Thanks Daniel Bendavid. Merge of [#702](https://github.com/mozilla/nunjucks/pull/702). 2.4.1 (Mar 17 2016) ------------------- * Don't double-escape. Thanks legutierr. Merge of [#701](https://github.com/mozilla/nunjucks/pull/701). * Prevent filter.escape from escaping SafeString. Thanks atian25. Merge of [#623](https://github.com/mozilla/nunjucks/pull/623). * Throw an error if a block is defined multiple times. Refs [#696](https://github.com/mozilla/nunjucks/issues/696). * Officially recommend the `.njk` extension. Thanks David Kebler. Merge of [#691](https://github.com/mozilla/nunjucks/pull/691). * Allow block-set to wrap an inheritance block. Unreported; fixed as a side effect of the fix for [#576](https://github.com/mozilla/nunjucks/issues/576). * Fix `filter` tag with non-trivial contents. Thanks Stefan Cruz and Fabien Franzen for report and investigation, Jan Oopkaup for failing tests. Fixes [#576](https://github.com/mozilla/nunjucks/issues/576). 2.4.0 (Mar 10 2016) ------------------- * Allow retrieving boolean-false as a global. Thanks Marius Büscher. Merge of [#694](https://github.com/mozilla/nunjucks/pull/694). * Don't automatically convert any for-loop that has an include statement into an async loop. Reverts [7d4716f4fd](https://github.com/mozilla/nunjucks/commit/7d4716f4fd), re-opens [#372](https://github.com/mozilla/nunjucks/issues/372), fixes [#527](https://github.com/mozilla/nunjucks/issues/527). Thanks Tom Delmas for the report. * Switch from Optimist to Yargs for argument-parsing. Thanks Bogdan Chadkin. Merge of [#672](https://github.com/mozilla/nunjucks/pull/672). * Prevent includes from writing to their including scope. Merge of [#667](https://github.com/mozilla/nunjucks/pull/667) (only partially backported to 2.x; macro var visibility not backported). * Fix handling of `dev` environment option, to get full tracebacks on errors (including nunjucks internals). Thanks Tobias Petry and Chandrasekhar Ambula V for the report, Aleksandr Motsjonov for draft patch. * Support using `in` operator to search in both arrays and objects, and it will throw an error for other data types. Fix [#659](https://github.com/mozilla/nunjucks/pull/659). Thanks Alex Mayfield for report and test, Ouyang Yadong for fix. Merge of [#661](https://github.com/mozilla/nunjucks/pull/661). * Add support for `{% set %}` block assignments as in jinja2. Thanks Daniele Rapagnani. Merge of [#656](https://github.com/mozilla/nunjucks/pull/656) * Fix `{% set %}` scoping within macros. Fixes [#577](https://github.com/mozilla/nunjucks/issues/577) and the macro portion of [#561](https://github.com/mozilla/nunjucks/issues/561). Thanks Ouyang Yadong. Merge of [#653](https://github.com/mozilla/nunjucks/pull/653). * Add support for named `endblock` (e.g. `{% endblock foo %}`). Thanks ricordisamoa. Merge of [#641](https://github.com/mozilla/nunjucks/pull/641). * Fix `range` global with zero as stop-value. Thanks Thomas Hunkapiller. Merge of [#638](https://github.com/mozilla/nunjucks/pull/638). * Fix a bug in urlize that collapsed whitespace. Thanks Paulo Bu. Merge of [#637](https://github.com/mozilla/nunjucks/pull/637). * Add `sum` filter. Thanks Pablo Matías Lazo. Merge of [#629](https://github.com/mozilla/nunjucks/pull/629). * Don't suppress errors inside {% if %} tags. Thanks Artemy Tregubenko for report and test, Ouyang Yadong for fix. Merge of [#634](https://github.com/mozilla/nunjucks/pull/634). * Allow whitespace control on comment blocks, too. Thanks Ouyang Yadong. Merge of [#632](https://github.com/mozilla/nunjucks/pull/632). * Fix whitespace control around nested tags/variables/comments. Thanks Ouyang Yadong. Merge of [#631](https://github.com/mozilla/nunjucks/pull/631). v2.3.0 (Jan 6 2016) ------------------- * Return `null` from `WebLoader` on missing template instead of throwing an error, for consistency with other loaders. This allows `WebLoader` to support the new `ignore missing` flag on the `include` tag. If `ignore missing` is not set, a generic "template not found" error will still be thrown, just like for any other loader. Ajax errors other than 404 will still cause `WebLoader` to throw an error directly. * Add preserve-linebreaks option to `striptags` filter. Thanks Ivan Kleshnin. Merge of [#619](https://github.com/mozilla/nunjucks/pull/619). v2.2.0 (Nov 23 2015) -------------------- * Add `striptags` filter. Thanks Anthony Giniers. Merge of [#589](https://github.com/mozilla/nunjucks/pull/589). * Allow compiled templates to be imported, included and extended. Thanks Luis Gutierrez-Sheris. Merge of [#581](https://github.com/mozilla/nunjucks/pull/581). * Fix issue with different nunjucks environments sharing same globals. Each environment is now independent. Thanks Paul Pechin. Merge of [#574](https://github.com/mozilla/nunjucks/pull/574). * Add negative steps support for range function. Thanks Nikita Mostovoy. Merge of [#575](https://github.com/mozilla/nunjucks/pull/575). * Remove deprecation warning when using the `default` filter without specifying a third argument. Merge of [#567](https://github.com/mozilla/nunjucks/pull/567). * Add support for chaining of addGlobal, addFilter, etc. Thanks Rob Graeber. Merge of [#537](https://github.com/mozilla/nunjucks/pull/537) * Fix error propagation. Thanks Tom Delmas. Merge of [#534](https://github.com/mozilla/nunjucks/pull/534). * trimBlocks now also trims windows style line endings. Thanks Magnus Tovslid. Merge of [#548](https://github.com/mozilla/nunjucks/pull/548) * `include` now supports an option to suppress errors if the template does not exist. Thanks Mathias Nestler. Merge of [#559](https://github.com/mozilla/nunjucks/pull/559) v2.1.0 (Sep 21 2015) -------------------- * Fix creating `WebLoader` without `opts`. Merge of [#524](https://github.com/mozilla/nunjucks/pull/524). * Add `hasExtension` and `removeExtension` methods to `Environment`. Merge of [#512](https://github.com/mozilla/nunjucks/pull/512). * Add support for kwargs in `sort` filter. Merge of [#510](https://github.com/mozilla/nunjucks/pull/510). * Add `none` as a lexed constant evaluating to `null`. Merge of [#480](https://github.com/mozilla/nunjucks/pull/480). * Fix rendering of multiple `raw` blocks. Thanks Aaron O'Mullan. Merge of [#503](https://github.com/mozilla/nunjucks/pull/503). * Avoid crashing on async loader error. Thanks Samy Pessé. Merge of [#504](https://github.com/mozilla/nunjucks/pull/504). * Add support for keyword arguments for sort filter. Thanks Andres Pardini. Merge of [#510](https://github.com/mozilla/nunjucks/pull/510) v2.0.0 (Aug 30 2015) -------------------- Most of the changes can be summed up in the [issues tagged 2.0](https://github.com/mozilla/nunjucks/issues?q=is%3Aissue+milestone%3A2.0+is%3Aclosed). Or you can [see all commits](https://github.com/mozilla/nunjucks/compare/v1.3.4...f8aabccefc31a9ffaccdc6797938b5187e07ea87). Most important changes: * **autoescape is now on by default.** You need to explicitly pass `{ autoescape: false }` in the options to turn it off. * **watch is off by default.** You need to explicitly pass `{ watch: true }` to start the watcher. * The `default` filter has changed. It will show the default value only if the argument is **undefined**. Any other value, even false-y values like `false` and `null`, will be returned. You can get back the old behavior by passing `true` as a 3rd argument to activate the loose-y behavior: `foo | default("bar", true)`. In 2.0 if you don't pass the 3rd argument, a warning will be displayed about this change in behavior. In 2.1 this warning will be removed. * [New filter tag](http://mozilla.github.io/nunjucks/templating.html#filter) * Lots of other bug fixes and small features, view the above issue list! v1.3.4 (Apr 27 2015) -------------------- This is an extremely minor release that only adds an .npmignore so that the bench, tests, and docs folders do not get published to npm. Nunjucks should download a lot faster now. v1.3.3 (Apr 3 2015) ------------------- This is exactly the same as v1.3.1, just fixing a typo in the git version tag. v1.3.2 (Apr 3 2015) ------------------- (no notes) v1.3.1 (Apr 3 2015) ------------------- We added strict mode to all the files, but that broke running nunjucks in the browser. Should work now with this small fix. v1.3.0 (Apr 3 2015) ------------------- * Relative templates: you can now load a template relatively by starting the path with ., like ./foo.html * FileSystemLoader now takes a noCache option, if true will disable caching entirely * Additional lstripBlocks and trimBlocks available to clean output automatically * New selectattr and rejectattr filters * Small fixes to the watcher * Several bug fixes v1.2.0 (Feb 4 2015) ------------------- * The special non-line-breaking space is considered whitespace now * The in operator has a lower precedence now. This is potentially a breaking change, thus the minor version bump. See [#336](https://github.com/mozilla/nunjucks/pull/336) * import with context now implemented: [#319](https://github.com/mozilla/nunjucks/pull/319) * async rendering doesn't throw compile errors v1.1.0 (Sep 30 2014) -------------------- User visible changes: * Fix a bug in urlize that would remove periods * custom tag syntax (like {% and %}) was made Environment-specific internally. Previously they were global even though you set them through the Environment. * Remove aggressive optimization that only emitted loop variables when uses. It introduced several bugs and didn't really improve perf. * Support the regular expression syntax like /foo/g. * The replace filter can take a regex as the first argument * The call tag was implemented * for tags can now take an else clause * The cycler object now exposes the current item as the current property * The chokidar library was updated and should fix various issues Dev changes: * Test coverage now available via istanbul. Will automatically display after running tests. v1.0.7 (Aug 15 2014) -------------------- Mixed up a few things in the 1.0.6 release, so another small bump. This merges in one thing: * The length filter will not throw an error is used on an undefined variable. It will return 0 if the variable is undefined. v1.0.6 (Aug 15 2014) -------------------- * Added the addGlobal method to the Environment object * import/extends/include now can take an arbitrary expression * fix bugs in set * improve express integration (allows rendering templates without an extension) v1.0.5 (May 1 2014) ------------------- * Added support for browserify * Added option to specify template output path when precompiling templates * Keep version comment in browser minified files * Speed up SafeString implementation * Handle null and non-matching cases for word count filter * Added support for node-webkit * Other various minor bugfixes chokidar repo fix - v1.0.4 (Apr 4 2014) --------------------------------------- * The chokidar dependency moved repos, and though the git URL should have been forwarded some people were having issues. This fixed the repo and version. (v1.0.3 is skipped because it was published with a bad URL, quickly fixed with another version bump) Bug fixes - v1.0.2 (Mar 25 2014) -------------------------------- * Use chokidar for watching file changes. This should fix a lot of problems on OS X machines. * Always use / in paths when precompiling templates * Fix bug where async filters hang indefinitely inside if statements * Extensions now can override autoescaping with an autoescape property * Other various minor bugfixes v1.0.1 (Dec 16, 2013) --------------------- (no notes) We've reached 1.0! Better APIs, asynchronous control, and more (Oct 24, 2013) ----------------------------------------------------------------------------- * An asynchronous API is now available, and async filters, extensions, and loaders is supported. The async API is optional and if you don't do anything async (the default), nothing changes for you. You can read more about this [here](http://jlongster.github.io/nunjucks/api.html#asynchronous-support). (fixes [#41](https://github.com/mozilla/nunjucks/issues/41)) * Much simpler higher-level API for initiating/configuring nunjucks is available. Read more [here](http://jlongster.github.io/nunjucks/api.html#simple-api). * An official grunt plugin is available for precompiling templates: [grunt-nunjucks](https://github.com/jlongster/grunt-nunjucks) * **The browser files have been renamed.** nunjucks.js is now the full library with compiler, and nunjucks-slim.js is the small version that only works with precompiled templates * urlencode filter has been added * The express integration has been refactored and isn't a kludge anymore. Should avoid some bugs and be more future-proof; * The order in which variables are lookup up in the context and frame lookup has been reversed. It will now look in the frame first, and then the context. This means that if a for loop introduces a new var, like {% for name in names %}, and if you have name in the context as well, it will properly reference name from the for loop inside the loop. (fixes [#122](https://github.com/mozilla/nunjucks/pull/122) and [#119](https://github.com/mozilla/nunjucks/issues/119)) v0.1.10 (Aug 9 2013) -------------------- (no notes) v0.1.9 (May 30 2013) -------------------- (no notes) v0.1.8 - whitespace controls, unpacking, better errors, and more! (Feb 6 2013) ------------------------------------------------------------------------------ There are lots of cool new features in this release, as well as many critical bug fixes. Full list of changes: * Whitespace control is implemented. Use {%- and -%} to strip whitespace before/after the block. * `for` loops implement Python-style array unpacking. This is a really nice feature which lets you do this: {% for x, y, z in [[2, 2, 2], [3, 3, 3]] %} --{{ x }} {{ y }} {{ z }}-- {% endfor %} The above would output: --2 2 2----3 3 3-- You can pass any number of variable names to for and it will destructure each array in the list to the variables. This makes the syntax between arrays and objects more consistent. Additionally, it allows us to implement the `dictsort` filter which sorts an object by keys or values. Technically, it returns an array of 2-value arrays and the unpacking takes care of it. Example: {% for k, v in { b: 2, a: 1 } %} --{{ k }}: {{ v }}-- {% endfor %} Output: `--b: 2----a: 1--` (note: the order could actually be anything because it uses javascript’s `for k in obj` syntax to iterate, and ordering depends on the js implementation) {% for k, v in { b: 2, a: 1} | dictsort %} --{{ k }}: {{ v }}-- {% endfor %} Output: `--a: 1----b: 2--` The above output will always be ordered that way. See the documentation for more details. Thanks to novocaine for this! * Much better error handling with at runtime (shows template/line/col information for attempting to call undefined values, etc) * Fixed a regression which broke the {% raw %} block * Fix some edge cases with variable lookups * Fix a regression with loading precompiled templates * Tweaks to allow usage with YUICompressor * Use the same error handling as normal when precompiling (shows proper errors) * Fix template loading on Windows machines * Fix int/float filters * Fix regression with super() v0.1.7 - helpful errors, many bug fixes (Dec 12 2012) ----------------------------------------------------- The biggest change in v0.1.7 comes from devoidfury (thanks!) which implements consistent and helpful error messages. The errors are still simply raw text, and not pretty HTML, but they at least contain all the necessary information to track down an error, such as template names, line and column numbers, and the inheritance stack. So if an error happens in a child template, it will print out all the templates that it inherits. In the future, we will most likely display the actual line causing an error. Full list of changes: * Consistent and helpful error messages * Expressions are more consistent now. Previously, there were several places that wouldn’t accept an arbitrary expression that should. For example, you can now do {% include templateNames['foo'] %}, whereas previously you could only give it a simply variable name. * app.locals is fixed with express 2.5 * Method calls on objects now have correct scope for this. Version 0.1.6 broke this and this was referencing the global scope. * A check was added to enforce loading of templates within the correct path. Previously you could load a file outside of the template with something like ../../crazyPrivateFile.txt You can [view all the code changes here](https://github.com/jlongster/nunjucks/compare/v0.1.6...v0.1.7). Please [file an issue](https://github.com/jlongster/nunjucks/issues?page=1&state=open) if something breaks! v0.1.6 - undefined handling, bugfixes (Nov 13, 2012) ---------------------------------------------------- This is mostly a bugfix release, but there are a few small tweaks based on feedback: * In some cases, backslashes in the template would not appear in the output. This has been fixed. * An error is thrown if a filter is not found * Old versions of express are now supported (2.5.11 was tested) * References on undefined objects are now suppressed. For example, {{ foo }}, {{ foo.bar }}, {{ foo.bar.baz }} all output nothing if foo is undefined. Previously only the first form would be suppressed, and a cryptic error thrown for the latter 2 references. Note: I believe this is a departure from jinja, which throws errors when referencing undefined objects. I feel that this is a good and non-breaking addition though. (thanks to devoidfury) * A bug in set where you couldn’t not reference other variables is fixed (thanks chriso and panta) * Other various small bugfixes You can view [all the code changes here](https://github.com/jlongster/nunjucks/compare/v0.1.5...v0.1.6). As always, [file an issue](https://github.com/jlongster/nunjucks/issues) if something breaks! v0.1.5 - macros, keyword arguments, bugfixes (Oct 11 2012) ---------------------------------------------------------- v0.1.5 has been pushed to npm, and it’s a big one. Please file any issues you find, and I’ll fix them as soon as possible! * The node data structure has been completely refactored to reduce redundancy and make it easier to add more types in the future. * Thanks to Brent Hagany, macros now have been implemented. They should act exactly the way jinja2 macros do. * A calling convention which implements keyword arguments now exists. All keyword args are converted into a hash and passed as the last argument. Macros needed this to implement keyword/default arguments. * Function and filter calls apply the new keyword argument calling convention * The “set” block now appropriately only sets a variable for the current scope. * Many other bugfixes. I’m watching this release carefully because of the large amount of code that has changed, so please [file an issue](https://github.com/jlongster/nunjucks/issues) if you have a problem with it. nunjucks-fd500902d7c88672470c87170796de52fc0f791a/CODE_OF_CONDUCT.md000664 000000 000000 00000001263 14012546311 022654 0ustar00rootroot000000 000000 # Community Participation Guidelines This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. nunjucks-fd500902d7c88672470c87170796de52fc0f791a/CONTRIBUTING.md000664 000000 000000 00000004153 14012546311 022307 0ustar00rootroot000000 000000 # Contributing Thanks for your interest in contributing! The advice below will help you get your issue fixed / pull request merged. ## Purpose Nunjucks has the following purpose: * Aim for templating feature parity with Jinja2. * Aim for templating feature parity with Twig, but only when not conflicting with Jinja2 parity. * Works in all node releases that are [actively maintained by the Node Foundation](https://github.com/nodejs/Release#release-schedule) * Works in all modern browsers (with [ES5 support](http://kangax.github.io/compat-table/es5/)). * Works in IE8 with [es5-shim](https://github.com/es-shims/es5-shim). * Keep footprint browser files as small as possible (save on bandwidth, download time). * Keep performance as fast as possible (see benchmarks). * Keep maintenance as easy as possible (avoid complexity, automate what we can). Notes: * We don't aim for parity of all language specific syntax. * We don't aim for parity of language specific filters like [Twig's PHP date format](http://twig.sensiolabs.org/doc/functions/date.html). Issues and pull requests contributing to this purpose have the best chance to make it into Nunjucks. ## Questions? Please DO NOT ask "how do I?" or usage questions via GitHub issues. Instead, use the [mailing list](https://groups.google.com/forum/#!forum/nunjucks). ## Submitting Issues Issues are easier to reproduce/resolve when they have: - A pull request with a failing test demonstrating the issue - A code example that produces the issue consistently - A traceback (when applicable) ## Pull Requests When creating a pull request: - Write tests (see below). - Note user-facing changes in the [`CHANGELOG.md`](CHANGELOG.md) file. - Update the documentation (in [`docs/`](docs/)) as needed. ## Testing Please add tests for any changes you submit. The tests should fail before your code changes, and pass with your changes. Existing tests should not break. Test coverage (output at the end of every test run) should never decrease after your changes. To install all the requirements for running the tests: ```bash npm install ``` To run the tests: ```bash npm test ``` nunjucks-fd500902d7c88672470c87170796de52fc0f791a/LICENSE000664 000000 000000 00000002437 14012546311 021066 0ustar00rootroot000000 000000 Copyright (c) 2012-2015, James Long All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.nunjucks-fd500902d7c88672470c87170796de52fc0f791a/MAINTENANCE.md000664 000000 000000 00000004510 14012546311 022117 0ustar00rootroot000000 000000 # Pushing a New Version Nunjucks attempts to adhere to semantic versioning. The API is very stable, so from here on out it will most likely be point releases. 1. Do a `pull` from github to make sure you have all the latest updates. 2. View all the changes since the last version: ``` $ git log --oneline v1.2.3..master ``` Replace `v1.2.3` with whatever the last version was, and you'll see all the changes going out in this version. Ensure that all significant user-facing changes (new features and bugfixes) are mentioned in `CHANGELOG.md`. Change the "master (unreleased)" heading in `CHANGELOG.md` to the new version number and date. 3. Update the version in `package.json`. 3. Run the command to update the ready-made files for the browser. ``` $ npm run browserfiles ``` 5. Commit above changes and push to `master` (or a release branch, if using one). 6. Draft a new release on GitHub and copy the changelog to the description. The tag and title should both be the version, in the form `v2.3.0`. Publish the release. 7. Publish to npm: ``` npm publish ``` 8. Make sure docs are up-to-date. You need to copy all the `nunjucks*.js` files in `browser/` to the docs. This is where the "download" link points to in the docs. You also need to copy the tests into the docs, for the online browser tests. ``make prod`` in the ``docs/`` dir will handle these tasks for you. Push (force push if necessary) the build out _site folder onto the `gh-pages` branch of the `nunjucks` repo to get it live. One way to do that is the following commands. These commands presume that you have another nunjucks git clone inside the (git-ignored) `docs/_site` directory, checked out to the `gh-pages` branch (and tracking `origin/gh-pages`). (To set that up the first time, `cd docs/_site`, `rm -rf *`, `git clone git@github.com:mozilla/nunjucks.git .`, and `git checkout gh-pages`). ``` cd docs && make prod cd files python -m SimpleHTTPServer # load http://localhost:8000/tests/browser/ and verify tests pass in browser cd ../_site && git add -A && git commit && git push ``` 9. Add a new "master (unreleased)" section at the top of `CHANGELOG.md`. 10. Bump the version number in `package.json` to a development pre-release of the next anticipated release number (e.g. "2.2.0-dev.1"). nunjucks-fd500902d7c88672470c87170796de52fc0f791a/README.md000664 000000 000000 00000004117 14012546311 021335 0ustar00rootroot000000 000000 # Nunjucks [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Linux Build][github-actions-image]][github-actions-url] [![Windows Build][appveyor-image]][appveyor-url] [![Test Codecov][codecov-image]][codecov-url] [Nunjucks](https://mozilla.github.io/nunjucks/) is a full featured templating engine for javascript. It is heavily inspired by [jinja2](http://jinja.pocoo.org/). View the docs [here](https://mozilla.github.io/nunjucks/). ## Installation `npm install nunjucks` To use the file watcher built-in to Nunjucks, Chokidar must be installed separately. `npm install nunjucks chokidar` (View the [CHANGELOG](https://github.com/mozilla/nunjucks/releases)) ## Documentation See [here](https://mozilla.github.io/nunjucks/). ## Browser Support Supported in all modern browsers. For IE8 support, use [es5-shim](https://github.com/es-shims/es5-shim). ## Tests Run the tests with `npm test`. Watch `master` branch's [tests running in the browser](https://mozilla.github.io/nunjucks/files/tests/browser/). ## Mailing List Join our mailing list and get help with and issues you have: https://groups.google.com/forum/?fromgroups#!forum/nunjucks ## Want to help? Contributions are always welcome! Before you submit an issue or pull request, please read our [contribution guidelines](CONTRIBUTING.md). [Contributors](https://github.com/mozilla/nunjucks/graphs/contributors) [npm-image]: https://img.shields.io/npm/v/nunjucks.svg [npm-url]: https://npmjs.org/package/nunjucks [downloads-image]: https://img.shields.io/npm/dm/nunjucks.svg [downloads-url]: https://npmjs.org/package/nunjucks [github-actions-image]: https://img.shields.io/github/workflow/status/mozilla/nunjucks/Tests/master.svg?label=linux [github-actions-url]: https://github.com/mozilla/nunjucks/actions [appveyor-image]: https://img.shields.io/appveyor/ci/fdintino/nunjucks/master.svg?label=windows [appveyor-url]: https://ci.appveyor.com/project/fdintino/nunjucks [codecov-image]: https://img.shields.io/codecov/c/gh/mozilla/nunjucks.svg [codecov-url]: https://codecov.io/gh/mozilla/nunjucks/branch/master nunjucks-fd500902d7c88672470c87170796de52fc0f791a/appveyor.yml000664 000000 000000 00000001241 14012546311 022441 0ustar00rootroot000000 000000 # Fix line endings in Windows. (runs before repo cloning) init: - git config --global core.autocrlf input # Test against these versions of Node.js. environment: matrix: - nodejs_version: "11" - nodejs_version: "10" - nodejs_version: "8" # Install scripts. (runs after repo cloning) install: # Get the latest stable version of Node.js or io.js - ps: Install-Product node $env:nodejs_version # install modules - npm install - npm install codecov # Post-install test scripts. test_script: # Output useful info for debugging. - node --version - npm --version # run tests - npm test - npm run codecov # Don't actually build. build: off nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/000775 000000 000000 00000000000 14012546311 021132 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/case.html000664 000000 000000 00000034743 14012546311 022746 0ustar00rootroot000000 000000

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %}

{{ header }}

{% if items.length %} {% else %}

The list is empty.

{% endif %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/index.html000664 000000 000000 00000002311 14012546311 023124 0ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/jinja/000775 000000 000000 00000000000 14012546311 022225 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/jinja/index.html000664 000000 000000 00000000046 14012546311 024222 0ustar00rootroot000000 000000 {% for x in foo %}{{ x }}{% endfor %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/jinja/jinja.py000664 000000 000000 00000001437 14012546311 023677 0ustar00rootroot000000 000000 import time from jinja2 import Template, Environment, FileSystemLoader env = Environment(loader=FileSystemLoader('.')) print env.get_template('index.html').render() # src = open('index.html').read() # print(env._generate(env._parse(src, 'poop', 'hello.html'), # 'poop', # 'hello.html')) # print([x for x in env._tokenize(src, 'poop', 'hello.html')]) # env = Environment(loader=FileSystemLoader('.')) # times = [] # arr = [5]*1000 # for i in range(100): # env = Environment(loader=FileSystemLoader('.')) # t1 = time.time() # tmpl = env.get_template('index.html') # tmpl.render({'username': 'james', # 'arr': arr}) # t2 = time.time() # times.append(t2-t1) # print( reduce(lambda x, y: x+y, times) / len(times)) nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bench/run.js000664 000000 000000 00000003506 14012546311 022300 0ustar00rootroot000000 000000 'use strict'; var fs = require('fs'); var bench = require('bench'); var oldNunjucks = require('nunjucks'); var nunjucks = require('../index'); var src = fs.readFileSync('case.html', 'utf-8'); var oldEnv = new oldNunjucks.Environment(null); var oldTmpl = new oldNunjucks.Template(src, env, null, null, true); var env = new nunjucks.Environment(null); var tmpl = new nunjucks.Template(src, env, null, null, true); var ctx = { items: [ { current: true, name: 'James' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' }, { name: 'Foo', url: 'http://example.com' } ] }; exports.time = 1000; exports.compareCount = 8; exports.compare = { 'old-nunjucks': function() { oldTmpl.render(ctx); }, 'new-nunjucks': function(done) { tmpl.render(ctx, done); } }; // var start = Date.now(); // function g() {} // for(var i=0; i<3000; i++) { // oldTmpl.render(ctx); // //tmpl.render(ctx, g); // } // console.log(Date.now() - start); bench.runMain(); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bin/000775 000000 000000 00000000000 14012546311 020623 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bin/precompile000775 000000 000000 00000004142 14012546311 022711 0ustar00rootroot000000 000000 #!/usr/bin/env node var {program} = require('commander'); var precompile = require('../src/precompile').precompile; var Environment = require('../src/environment').Environment; var lib = require('../src/lib'); var cmdpath = null; program .storeOptionsAsProperties(false) .passCommandToAction(false); program .name('precompile') .usage('[-f|--force] [-a|--filters ] [-n|--name ] [-i|--include ] [-x|--exclude ] [-w|--wrapper ] ') .arguments('') .helpOption('-?, -h, --help', 'Display this help message') .option('-f, --force', 'Force compilation to continue on error') .option('-a, --filters ', 'Give the compiler a comma-delimited list of asynchronous filters, required for correctly generating code') .option('-n, --name ', 'Specify the template name when compiling a single file') .option('-i, --include ', 'Include a file or folder which match the regex but would otherwise be excluded. You can use this flag multiple times', concat, ['\\.html$', '\\.jinja$']) .option('-x, --exclude ', 'Exclude a file or folder which match the regex but would otherwise be included. You can use this flag multiple times', concat, []) .option('-w, --wrapper ', 'Load a external plugin to change the output format of the precompiled templates (for example, "-w custom" will load a module named "nunjucks-custom")') .action(function (path) { cmdpath = path; }) .parse(process.argv); function concat(value, previous) { return previous.concat(value); } if (cmdpath == null) { program.outputHelp(); console.error('\nerror: no path given'); process.exit(1); } var env = new Environment([]); const opts = program.opts(); lib.each([].concat(opts.filters).join(',').split(','), function (name) { env.addFilter(name.trim(), function () {}, true); }); if (opts.wrapper) { opts.wrapper = require('nunjucks-' + opts.wrapper).wrapper; } console.log(precompile(cmdpath, { env : env, force : opts.force, name : opts.name, wrapper: opts.wrapper, include : [].concat(opts.include), exclude : [].concat(opts.exclude) })); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bin/precompile.cmd000664 000000 000000 00000000157 14012546311 023452 0ustar00rootroot000000 000000 @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\precompile" %* ) ELSE ( node "%~dp0\precompile" %* )nunjucks-fd500902d7c88672470c87170796de52fc0f791a/bower.json000664 000000 000000 00000000414 14012546311 022063 0ustar00rootroot000000 000000 { "name": "nunjucks", "main": "browser/nunjucks.js", "ignore": [ "Makefile", "package.json", "tests", "docs", "index.js", "nunjucks" ], "keywords": [ "jinja", "template", "templates", "templating", "browser" ] } nunjucks-fd500902d7c88672470c87170796de52fc0f791a/codecov.yml000664 000000 000000 00000000040 14012546311 022212 0ustar00rootroot000000 000000 comment: require_changes: yes nunjucks-fd500902d7c88672470c87170796de52fc0f791a/contribute.json000664 000000 000000 00000001260 14012546311 023123 0ustar00rootroot000000 000000 { "name": "Nunjucks", "description": "Nunjucks is a full featured templating engine for javascript. It is heavily inspired by jinja2.", "repository": { "url": "https://github.com/mozilla/nunjucks", "license": "BSD 2-Clause", "tests": "https://travis-ci.org/mozilla/nunjucks" }, "participate": { "home": "https://github.com/mozilla/nunjucks", "docs": "https://github.com/mozilla/nunjucks" }, "bugs": { "list": "https://github.com/mozilla/nunjucks/issues", "report": "https://github.com/mozilla/nunjucks/issues/new" }, "keywords": [ "javascript", "node.js", "jinja" ] } nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/000775 000000 000000 00000000000 14012546311 021713 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/index.js000664 000000 000000 00000004250 14012546311 023361 0ustar00rootroot000000 000000 'use strict'; const lib = require('./src/lib'); const {Environment, Template} = require('./src/environment'); const Loader = require('./src/loader'); const loaders = require('./src/loaders'); const precompile = require('./src/precompile'); const compiler = require('./src/compiler'); const parser = require('./src/parser'); const lexer = require('./src/lexer'); const runtime = require('./src/runtime'); const nodes = require('./src/nodes'); const installJinjaCompat = require('./src/jinja-compat'); // A single instance of an environment, since this is so commonly used let e; function configure(templatesPath, opts) { opts = opts || {}; if (lib.isObject(templatesPath)) { opts = templatesPath; templatesPath = null; } let TemplateLoader; if (loaders.FileSystemLoader) { TemplateLoader = new loaders.FileSystemLoader(templatesPath, { watch: opts.watch, noCache: opts.noCache }); } else if (loaders.WebLoader) { TemplateLoader = new loaders.WebLoader(templatesPath, { useCache: opts.web && opts.web.useCache, async: opts.web && opts.web.async }); } e = new Environment(TemplateLoader, opts); if (opts && opts.express) { e.express(opts.express); } return e; } module.exports = { Environment: Environment, Template: Template, Loader: Loader, FileSystemLoader: loaders.FileSystemLoader, NodeResolveLoader: loaders.NodeResolveLoader, PrecompiledLoader: loaders.PrecompiledLoader, WebLoader: loaders.WebLoader, compiler: compiler, parser: parser, lexer: lexer, runtime: runtime, lib: lib, nodes: nodes, installJinjaCompat: installJinjaCompat, configure: configure, reset() { e = undefined; }, compile(src, env, path, eagerCompile) { if (!e) { configure(); } return new Template(src, env, path, eagerCompile); }, render(name, ctx, cb) { if (!e) { configure(); } return e.render(name, ctx, cb); }, renderString(src, ctx, cb) { if (!e) { configure(); } return e.renderString(src, ctx, cb); }, precompile: (precompile) ? precompile.precompile : undefined, precompileString: (precompile) ? precompile.precompileString : undefined, }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/000775 000000 000000 00000000000 14012546311 022502 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/compiler.js000664 000000 000000 00000100425 14012546311 024654 0ustar00rootroot000000 000000 'use strict'; const parser = require('./parser'); const transformer = require('./transformer'); const nodes = require('./nodes'); const {TemplateError} = require('./lib'); const {Frame} = require('./runtime'); const {Obj} = require('./object'); // These are all the same for now, but shouldn't be passed straight // through const compareOps = { '==': '==', '===': '===', '!=': '!=', '!==': '!==', '<': '<', '>': '>', '<=': '<=', '>=': '>=' }; class Compiler extends Obj { init(templateName, throwOnUndefined) { this.templateName = templateName; this.codebuf = []; this.lastId = 0; this.buffer = null; this.bufferStack = []; this._scopeClosers = ''; this.inBlock = false; this.throwOnUndefined = throwOnUndefined; } fail(msg, lineno, colno) { if (lineno !== undefined) { lineno += 1; } if (colno !== undefined) { colno += 1; } throw new TemplateError(msg, lineno, colno); } _pushBuffer() { const id = this._tmpid(); this.bufferStack.push(this.buffer); this.buffer = id; this._emit(`var ${this.buffer} = "";`); return id; } _popBuffer() { this.buffer = this.bufferStack.pop(); } _emit(code) { this.codebuf.push(code); } _emitLine(code) { this._emit(code + '\n'); } _emitLines(...lines) { lines.forEach((line) => this._emitLine(line)); } _emitFuncBegin(node, name) { this.buffer = 'output'; this._scopeClosers = ''; this._emitLine(`function ${name}(env, context, frame, runtime, cb) {`); this._emitLine(`var lineno = ${node.lineno};`); this._emitLine(`var colno = ${node.colno};`); this._emitLine(`var ${this.buffer} = "";`); this._emitLine('try {'); } _emitFuncEnd(noReturn) { if (!noReturn) { this._emitLine('cb(null, ' + this.buffer + ');'); } this._closeScopeLevels(); this._emitLine('} catch (e) {'); this._emitLine(' cb(runtime.handleError(e, lineno, colno));'); this._emitLine('}'); this._emitLine('}'); this.buffer = null; } _addScopeLevel() { this._scopeClosers += '})'; } _closeScopeLevels() { this._emitLine(this._scopeClosers + ';'); this._scopeClosers = ''; } _withScopedSyntax(func) { var _scopeClosers = this._scopeClosers; this._scopeClosers = ''; func.call(this); this._closeScopeLevels(); this._scopeClosers = _scopeClosers; } _makeCallback(res) { var err = this._tmpid(); return 'function(' + err + (res ? ',' + res : '') + ') {\n' + 'if(' + err + ') { cb(' + err + '); return; }'; } _tmpid() { this.lastId++; return 't_' + this.lastId; } _templateName() { return this.templateName == null ? 'undefined' : JSON.stringify(this.templateName); } _compileChildren(node, frame) { node.children.forEach((child) => { this.compile(child, frame); }); } _compileAggregate(node, frame, startChar, endChar) { if (startChar) { this._emit(startChar); } node.children.forEach((child, i) => { if (i > 0) { this._emit(','); } this.compile(child, frame); }); if (endChar) { this._emit(endChar); } } _compileExpression(node, frame) { // TODO: I'm not really sure if this type check is worth it or // not. this.assertType( node, nodes.Literal, nodes.Symbol, nodes.Group, nodes.Array, nodes.Dict, nodes.FunCall, nodes.Caller, nodes.Filter, nodes.LookupVal, nodes.Compare, nodes.InlineIf, nodes.In, nodes.Is, nodes.And, nodes.Or, nodes.Not, nodes.Add, nodes.Concat, nodes.Sub, nodes.Mul, nodes.Div, nodes.FloorDiv, nodes.Mod, nodes.Pow, nodes.Neg, nodes.Pos, nodes.Compare, nodes.NodeList ); this.compile(node, frame); } assertType(node, ...types) { if (!types.some(t => node instanceof t)) { this.fail(`assertType: invalid type: ${node.typename}`, node.lineno, node.colno); } } compileCallExtension(node, frame, async) { var args = node.args; var contentArgs = node.contentArgs; var autoescape = typeof node.autoescape === 'boolean' ? node.autoescape : true; if (!async) { this._emit(`${this.buffer} += runtime.suppressValue(`); } this._emit(`env.getExtension("${node.extName}")["${node.prop}"](`); this._emit('context'); if (args || contentArgs) { this._emit(','); } if (args) { if (!(args instanceof nodes.NodeList)) { this.fail('compileCallExtension: arguments must be a NodeList, ' + 'use `parser.parseSignature`'); } args.children.forEach((arg, i) => { // Tag arguments are passed normally to the call. Note // that keyword arguments are turned into a single js // object as the last argument, if they exist. this._compileExpression(arg, frame); if (i !== args.children.length - 1 || contentArgs.length) { this._emit(','); } }); } if (contentArgs.length) { contentArgs.forEach((arg, i) => { if (i > 0) { this._emit(','); } if (arg) { this._emitLine('function(cb) {'); this._emitLine('if(!cb) { cb = function(err) { if(err) { throw err; }}}'); const id = this._pushBuffer(); this._withScopedSyntax(() => { this.compile(arg, frame); this._emitLine(`cb(null, ${id});`); }); this._popBuffer(); this._emitLine(`return ${id};`); this._emitLine('}'); } else { this._emit('null'); } }); } if (async) { const res = this._tmpid(); this._emitLine(', ' + this._makeCallback(res)); this._emitLine( `${this.buffer} += runtime.suppressValue(${res}, ${autoescape} && env.opts.autoescape);`); this._addScopeLevel(); } else { this._emit(')'); this._emit(`, ${autoescape} && env.opts.autoescape);\n`); } } compileCallExtensionAsync(node, frame) { this.compileCallExtension(node, frame, true); } compileNodeList(node, frame) { this._compileChildren(node, frame); } compileLiteral(node) { if (typeof node.value === 'string') { let val = node.value.replace(/\\/g, '\\\\'); val = val.replace(/"/g, '\\"'); val = val.replace(/\n/g, '\\n'); val = val.replace(/\r/g, '\\r'); val = val.replace(/\t/g, '\\t'); val = val.replace(/\u2028/g, '\\u2028'); this._emit(`"${val}"`); } else if (node.value === null) { this._emit('null'); } else { this._emit(node.value.toString()); } } compileSymbol(node, frame) { var name = node.value; var v = frame.lookup(name); if (v) { this._emit(v); } else { this._emit('runtime.contextOrFrameLookup(' + 'context, frame, "' + name + '")'); } } compileGroup(node, frame) { this._compileAggregate(node, frame, '(', ')'); } compileArray(node, frame) { this._compileAggregate(node, frame, '[', ']'); } compileDict(node, frame) { this._compileAggregate(node, frame, '{', '}'); } compilePair(node, frame) { var key = node.key; var val = node.value; if (key instanceof nodes.Symbol) { key = new nodes.Literal(key.lineno, key.colno, key.value); } else if (!(key instanceof nodes.Literal && typeof key.value === 'string')) { this.fail('compilePair: Dict keys must be strings or names', key.lineno, key.colno); } this.compile(key, frame); this._emit(': '); this._compileExpression(val, frame); } compileInlineIf(node, frame) { this._emit('('); this.compile(node.cond, frame); this._emit('?'); this.compile(node.body, frame); this._emit(':'); if (node.else_ !== null) { this.compile(node.else_, frame); } else { this._emit('""'); } this._emit(')'); } compileIn(node, frame) { this._emit('runtime.inOperator('); this.compile(node.left, frame); this._emit(','); this.compile(node.right, frame); this._emit(')'); } compileIs(node, frame) { // first, we need to try to get the name of the test function, if it's a // callable (i.e., has args) and not a symbol. var right = node.right.name ? node.right.name.value // otherwise go with the symbol value : node.right.value; this._emit('env.getTest("' + right + '").call(context, '); this.compile(node.left, frame); // compile the arguments for the callable if they exist if (node.right.args) { this._emit(','); this.compile(node.right.args, frame); } this._emit(') === true'); } _binOpEmitter(node, frame, str) { this.compile(node.left, frame); this._emit(str); this.compile(node.right, frame); } // ensure concatenation instead of addition // by adding empty string in between compileOr(node, frame) { return this._binOpEmitter(node, frame, ' || '); } compileAnd(node, frame) { return this._binOpEmitter(node, frame, ' && '); } compileAdd(node, frame) { return this._binOpEmitter(node, frame, ' + '); } compileConcat(node, frame) { return this._binOpEmitter(node, frame, ' + "" + '); } compileSub(node, frame) { return this._binOpEmitter(node, frame, ' - '); } compileMul(node, frame) { return this._binOpEmitter(node, frame, ' * '); } compileDiv(node, frame) { return this._binOpEmitter(node, frame, ' / '); } compileMod(node, frame) { return this._binOpEmitter(node, frame, ' % '); } compileNot(node, frame) { this._emit('!'); this.compile(node.target, frame); } compileFloorDiv(node, frame) { this._emit('Math.floor('); this.compile(node.left, frame); this._emit(' / '); this.compile(node.right, frame); this._emit(')'); } compilePow(node, frame) { this._emit('Math.pow('); this.compile(node.left, frame); this._emit(', '); this.compile(node.right, frame); this._emit(')'); } compileNeg(node, frame) { this._emit('-'); this.compile(node.target, frame); } compilePos(node, frame) { this._emit('+'); this.compile(node.target, frame); } compileCompare(node, frame) { this.compile(node.expr, frame); node.ops.forEach((op) => { this._emit(` ${compareOps[op.type]} `); this.compile(op.expr, frame); }); } compileLookupVal(node, frame) { this._emit('runtime.memberLookup(('); this._compileExpression(node.target, frame); this._emit('),'); this._compileExpression(node.val, frame); this._emit(')'); } _getNodeName(node) { switch (node.typename) { case 'Symbol': return node.value; case 'FunCall': return 'the return value of (' + this._getNodeName(node.name) + ')'; case 'LookupVal': return this._getNodeName(node.target) + '["' + this._getNodeName(node.val) + '"]'; case 'Literal': return node.value.toString(); default: return '--expression--'; } } compileFunCall(node, frame) { // Keep track of line/col info at runtime by settings // variables within an expression. An expression in javascript // like (x, y, z) returns the last value, and x and y can be // anything this._emit('(lineno = ' + node.lineno + ', colno = ' + node.colno + ', '); this._emit('runtime.callWrap('); // Compile it as normal. this._compileExpression(node.name, frame); // Output the name of what we're calling so we can get friendly errors // if the lookup fails. this._emit(', "' + this._getNodeName(node.name).replace(/"/g, '\\"') + '", context, '); this._compileAggregate(node.args, frame, '[', '])'); this._emit(')'); } compileFilter(node, frame) { var name = node.name; this.assertType(name, nodes.Symbol); this._emit('env.getFilter("' + name.value + '").call(context, '); this._compileAggregate(node.args, frame); this._emit(')'); } compileFilterAsync(node, frame) { var name = node.name; var symbol = node.symbol.value; this.assertType(name, nodes.Symbol); frame.set(symbol, symbol); this._emit('env.getFilter("' + name.value + '").call(context, '); this._compileAggregate(node.args, frame); this._emitLine(', ' + this._makeCallback(symbol)); this._addScopeLevel(); } compileKeywordArgs(node, frame) { this._emit('runtime.makeKeywordArgs('); this.compileDict(node, frame); this._emit(')'); } compileSet(node, frame) { var ids = []; // Lookup the variable names for each identifier and create // new ones if necessary node.targets.forEach((target) => { var name = target.value; var id = frame.lookup(name); if (id === null || id === undefined) { id = this._tmpid(); // Note: This relies on js allowing scope across // blocks, in case this is created inside an `if` this._emitLine('var ' + id + ';'); } ids.push(id); }); if (node.value) { this._emit(ids.join(' = ') + ' = '); this._compileExpression(node.value, frame); this._emitLine(';'); } else { this._emit(ids.join(' = ') + ' = '); this.compile(node.body, frame); this._emitLine(';'); } node.targets.forEach((target, i) => { var id = ids[i]; var name = target.value; // We are running this for every var, but it's very // uncommon to assign to multiple vars anyway this._emitLine(`frame.set("${name}", ${id}, true);`); this._emitLine('if(frame.topLevel) {'); this._emitLine(`context.setVariable("${name}", ${id});`); this._emitLine('}'); if (name.charAt(0) !== '_') { this._emitLine('if(frame.topLevel) {'); this._emitLine(`context.addExport("${name}", ${id});`); this._emitLine('}'); } }); } compileSwitch(node, frame) { this._emit('switch ('); this.compile(node.expr, frame); this._emit(') {'); node.cases.forEach((c, i) => { this._emit('case '); this.compile(c.cond, frame); this._emit(': '); this.compile(c.body, frame); // preserve fall-throughs if (c.body.children.length) { this._emitLine('break;'); } }); if (node.default) { this._emit('default:'); this.compile(node.default, frame); } this._emit('}'); } compileIf(node, frame, async) { this._emit('if('); this._compileExpression(node.cond, frame); this._emitLine(') {'); this._withScopedSyntax(() => { this.compile(node.body, frame); if (async) { this._emit('cb()'); } }); if (node.else_) { this._emitLine('}\nelse {'); this._withScopedSyntax(() => { this.compile(node.else_, frame); if (async) { this._emit('cb()'); } }); } else if (async) { this._emitLine('}\nelse {'); this._emit('cb()'); } this._emitLine('}'); } compileIfAsync(node, frame) { this._emit('(function(cb) {'); this.compileIf(node, frame, true); this._emit('})(' + this._makeCallback()); this._addScopeLevel(); } _emitLoopBindings(node, arr, i, len) { const bindings = [ {name: 'index', val: `${i} + 1`}, {name: 'index0', val: i}, {name: 'revindex', val: `${len} - ${i}`}, {name: 'revindex0', val: `${len} - ${i} - 1`}, {name: 'first', val: `${i} === 0`}, {name: 'last', val: `${i} === ${len} - 1`}, {name: 'length', val: len}, ]; bindings.forEach((b) => { this._emitLine(`frame.set("loop.${b.name}", ${b.val});`); }); } compileFor(node, frame) { // Some of this code is ugly, but it keeps the generated code // as fast as possible. ForAsync also shares some of this, but // not much. const i = this._tmpid(); const len = this._tmpid(); const arr = this._tmpid(); frame = frame.push(); this._emitLine('frame = frame.push();'); this._emit(`var ${arr} = `); this._compileExpression(node.arr, frame); this._emitLine(';'); this._emit(`if(${arr}) {`); this._emitLine(arr + ' = runtime.fromIterator(' + arr + ');'); // If multiple names are passed, we need to bind them // appropriately if (node.name instanceof nodes.Array) { this._emitLine(`var ${i};`); // The object could be an arroy or object. Note that the // body of the loop is duplicated for each condition, but // we are optimizing for speed over size. this._emitLine(`if(runtime.isArray(${arr})) {`); this._emitLine(`var ${len} = ${arr}.length;`); this._emitLine(`for(${i}=0; ${i} < ${arr}.length; ${i}++) {`); // Bind each declared var node.name.children.forEach((child, u) => { var tid = this._tmpid(); this._emitLine(`var ${tid} = ${arr}[${i}][${u}];`); this._emitLine(`frame.set("${child}", ${arr}[${i}][${u}]);`); frame.set(node.name.children[u].value, tid); }); this._emitLoopBindings(node, arr, i, len); this._withScopedSyntax(() => { this.compile(node.body, frame); }); this._emitLine('}'); this._emitLine('} else {'); // Iterate over the key/values of an object const [key, val] = node.name.children; const k = this._tmpid(); const v = this._tmpid(); frame.set(key.value, k); frame.set(val.value, v); this._emitLine(`${i} = -1;`); this._emitLine(`var ${len} = runtime.keys(${arr}).length;`); this._emitLine(`for(var ${k} in ${arr}) {`); this._emitLine(`${i}++;`); this._emitLine(`var ${v} = ${arr}[${k}];`); this._emitLine(`frame.set("${key.value}", ${k});`); this._emitLine(`frame.set("${val.value}", ${v});`); this._emitLoopBindings(node, arr, i, len); this._withScopedSyntax(() => { this.compile(node.body, frame); }); this._emitLine('}'); this._emitLine('}'); } else { // Generate a typical array iteration const v = this._tmpid(); frame.set(node.name.value, v); this._emitLine(`var ${len} = ${arr}.length;`); this._emitLine(`for(var ${i}=0; ${i} < ${arr}.length; ${i}++) {`); this._emitLine(`var ${v} = ${arr}[${i}];`); this._emitLine(`frame.set("${node.name.value}", ${v});`); this._emitLoopBindings(node, arr, i, len); this._withScopedSyntax(() => { this.compile(node.body, frame); }); this._emitLine('}'); } this._emitLine('}'); if (node.else_) { this._emitLine('if (!' + len + ') {'); this.compile(node.else_, frame); this._emitLine('}'); } this._emitLine('frame = frame.pop();'); } _compileAsyncLoop(node, frame, parallel) { // This shares some code with the For tag, but not enough to // worry about. This iterates across an object asynchronously, // but not in parallel. var i = this._tmpid(); var len = this._tmpid(); var arr = this._tmpid(); var asyncMethod = parallel ? 'asyncAll' : 'asyncEach'; frame = frame.push(); this._emitLine('frame = frame.push();'); this._emit('var ' + arr + ' = runtime.fromIterator('); this._compileExpression(node.arr, frame); this._emitLine(');'); if (node.name instanceof nodes.Array) { const arrayLen = node.name.children.length; this._emit(`runtime.${asyncMethod}(${arr}, ${arrayLen}, function(`); node.name.children.forEach((name) => { this._emit(`${name.value},`); }); this._emit(i + ',' + len + ',next) {'); node.name.children.forEach((name) => { const id = name.value; frame.set(id, id); this._emitLine(`frame.set("${id}", ${id});`); }); } else { const id = node.name.value; this._emitLine(`runtime.${asyncMethod}(${arr}, 1, function(${id}, ${i}, ${len},next) {`); this._emitLine('frame.set("' + id + '", ' + id + ');'); frame.set(id, id); } this._emitLoopBindings(node, arr, i, len); this._withScopedSyntax(() => { let buf; if (parallel) { buf = this._pushBuffer(); } this.compile(node.body, frame); this._emitLine('next(' + i + (buf ? ',' + buf : '') + ');'); if (parallel) { this._popBuffer(); } }); const output = this._tmpid(); this._emitLine('}, ' + this._makeCallback(output)); this._addScopeLevel(); if (parallel) { this._emitLine(this.buffer + ' += ' + output + ';'); } if (node.else_) { this._emitLine('if (!' + arr + '.length) {'); this.compile(node.else_, frame); this._emitLine('}'); } this._emitLine('frame = frame.pop();'); } compileAsyncEach(node, frame) { this._compileAsyncLoop(node, frame); } compileAsyncAll(node, frame) { this._compileAsyncLoop(node, frame, true); } _compileMacro(node, frame) { var args = []; var kwargs = null; var funcId = 'macro_' + this._tmpid(); var keepFrame = (frame !== undefined); // Type check the definition of the args node.args.children.forEach((arg, i) => { if (i === node.args.children.length - 1 && arg instanceof nodes.Dict) { kwargs = arg; } else { this.assertType(arg, nodes.Symbol); args.push(arg); } }); const realNames = [...args.map((n) => `l_${n.value}`), 'kwargs']; // Quoted argument names const argNames = args.map((n) => `"${n.value}"`); const kwargNames = ((kwargs && kwargs.children) || []).map((n) => `"${n.key.value}"`); // We pass a function to makeMacro which destructures the // arguments so support setting positional args with keywords // args and passing keyword args as positional args // (essentially default values). See runtime.js. let currFrame; if (keepFrame) { currFrame = frame.push(true); } else { currFrame = new Frame(); } this._emitLines( `var ${funcId} = runtime.makeMacro(`, `[${argNames.join(', ')}], `, `[${kwargNames.join(', ')}], `, `function (${realNames.join(', ')}) {`, 'var callerFrame = frame;', 'frame = ' + ((keepFrame) ? 'frame.push(true);' : 'new runtime.Frame();'), 'kwargs = kwargs || {};', 'if (Object.prototype.hasOwnProperty.call(kwargs, "caller")) {', 'frame.set("caller", kwargs.caller); }'); // Expose the arguments to the template. Don't need to use // random names because the function // will create a new run-time scope for us args.forEach((arg) => { this._emitLine(`frame.set("${arg.value}", l_${arg.value});`); currFrame.set(arg.value, `l_${arg.value}`); }); // Expose the keyword arguments if (kwargs) { kwargs.children.forEach((pair) => { const name = pair.key.value; this._emit(`frame.set("${name}", `); this._emit(`Object.prototype.hasOwnProperty.call(kwargs, "${name}")`); this._emit(` ? kwargs["${name}"] : `); this._compileExpression(pair.value, currFrame); this._emit(');'); }); } const bufferId = this._pushBuffer(); this._withScopedSyntax(() => { this.compile(node.body, currFrame); }); this._emitLine('frame = ' + ((keepFrame) ? 'frame.pop();' : 'callerFrame;')); this._emitLine(`return new runtime.SafeString(${bufferId});`); this._emitLine('});'); this._popBuffer(); return funcId; } compileMacro(node, frame) { var funcId = this._compileMacro(node); // Expose the macro to the templates var name = node.name.value; frame.set(name, funcId); if (frame.parent) { this._emitLine(`frame.set("${name}", ${funcId});`); } else { if (node.name.value.charAt(0) !== '_') { this._emitLine(`context.addExport("${name}");`); } this._emitLine(`context.setVariable("${name}", ${funcId});`); } } compileCaller(node, frame) { // basically an anonymous "macro expression" this._emit('(function (){'); const funcId = this._compileMacro(node, frame); this._emit(`return ${funcId};})()`); } _compileGetTemplate(node, frame, eagerCompile, ignoreMissing) { const parentTemplateId = this._tmpid(); const parentName = this._templateName(); const cb = this._makeCallback(parentTemplateId); const eagerCompileArg = (eagerCompile) ? 'true' : 'false'; const ignoreMissingArg = (ignoreMissing) ? 'true' : 'false'; this._emit('env.getTemplate('); this._compileExpression(node.template, frame); this._emitLine(`, ${eagerCompileArg}, ${parentName}, ${ignoreMissingArg}, ${cb}`); return parentTemplateId; } compileImport(node, frame) { const target = node.target.value; const id = this._compileGetTemplate(node, frame, false, false); this._addScopeLevel(); this._emitLine(id + '.getExported(' + (node.withContext ? 'context.getVariables(), frame, ' : '') + this._makeCallback(id)); this._addScopeLevel(); frame.set(target, id); if (frame.parent) { this._emitLine(`frame.set("${target}", ${id});`); } else { this._emitLine(`context.setVariable("${target}", ${id});`); } } compileFromImport(node, frame) { const importedId = this._compileGetTemplate(node, frame, false, false); this._addScopeLevel(); this._emitLine(importedId + '.getExported(' + (node.withContext ? 'context.getVariables(), frame, ' : '') + this._makeCallback(importedId)); this._addScopeLevel(); node.names.children.forEach((nameNode) => { var name; var alias; var id = this._tmpid(); if (nameNode instanceof nodes.Pair) { name = nameNode.key.value; alias = nameNode.value.value; } else { name = nameNode.value; alias = name; } this._emitLine(`if(Object.prototype.hasOwnProperty.call(${importedId}, "${name}")) {`); this._emitLine(`var ${id} = ${importedId}.${name};`); this._emitLine('} else {'); this._emitLine(`cb(new Error("cannot import '${name}'")); return;`); this._emitLine('}'); frame.set(alias, id); if (frame.parent) { this._emitLine(`frame.set("${alias}", ${id});`); } else { this._emitLine(`context.setVariable("${alias}", ${id});`); } }); } compileBlock(node) { var id = this._tmpid(); // If we are executing outside a block (creating a top-level // block), we really don't want to execute its code because it // will execute twice: once when the child template runs and // again when the parent template runs. Note that blocks // within blocks will *always* execute immediately *and* // wherever else they are invoked (like used in a parent // template). This may have behavioral differences from jinja // because blocks can have side effects, but it seems like a // waste of performance to always execute huge top-level // blocks twice if (!this.inBlock) { this._emit('(parentTemplate ? function(e, c, f, r, cb) { cb(""); } : '); } this._emit(`context.getBlock("${node.name.value}")`); if (!this.inBlock) { this._emit(')'); } this._emitLine('(env, context, frame, runtime, ' + this._makeCallback(id)); this._emitLine(`${this.buffer} += ${id};`); this._addScopeLevel(); } compileSuper(node, frame) { var name = node.blockName.value; var id = node.symbol.value; const cb = this._makeCallback(id); this._emitLine(`context.getSuper(env, "${name}", b_${name}, frame, runtime, ${cb}`); this._emitLine(`${id} = runtime.markSafe(${id});`); this._addScopeLevel(); frame.set(id, id); } compileExtends(node, frame) { var k = this._tmpid(); const parentTemplateId = this._compileGetTemplate(node, frame, true, false); // extends is a dynamic tag and can occur within a block like // `if`, so if this happens we need to capture the parent // template in the top-level scope this._emitLine(`parentTemplate = ${parentTemplateId}`); this._emitLine(`for(var ${k} in parentTemplate.blocks) {`); this._emitLine(`context.addBlock(${k}, parentTemplate.blocks[${k}]);`); this._emitLine('}'); this._addScopeLevel(); } compileInclude(node, frame) { this._emitLine('var tasks = [];'); this._emitLine('tasks.push('); this._emitLine('function(callback) {'); const id = this._compileGetTemplate(node, frame, false, node.ignoreMissing); this._emitLine(`callback(null,${id});});`); this._emitLine('});'); const id2 = this._tmpid(); this._emitLine('tasks.push('); this._emitLine('function(template, callback){'); this._emitLine('template.render(context.getVariables(), frame, ' + this._makeCallback(id2)); this._emitLine('callback(null,' + id2 + ');});'); this._emitLine('});'); this._emitLine('tasks.push('); this._emitLine('function(result, callback){'); this._emitLine(`${this.buffer} += result;`); this._emitLine('callback(null);'); this._emitLine('});'); this._emitLine('env.waterfall(tasks, function(){'); this._addScopeLevel(); } compileTemplateData(node, frame) { this.compileLiteral(node, frame); } compileCapture(node, frame) { // we need to temporarily override the current buffer id as 'output' // so the set block writes to the capture output instead of the buffer var buffer = this.buffer; this.buffer = 'output'; this._emitLine('(function() {'); this._emitLine('var output = "";'); this._withScopedSyntax(() => { this.compile(node.body, frame); }); this._emitLine('return output;'); this._emitLine('})()'); // and of course, revert back to the old buffer id this.buffer = buffer; } compileOutput(node, frame) { const children = node.children; children.forEach(child => { // TemplateData is a special case because it is never // autoescaped, so simply output it for optimization if (child instanceof nodes.TemplateData) { if (child.value) { this._emit(`${this.buffer} += `); this.compileLiteral(child, frame); this._emitLine(';'); } } else { this._emit(`${this.buffer} += runtime.suppressValue(`); if (this.throwOnUndefined) { this._emit('runtime.ensureDefined('); } this.compile(child, frame); if (this.throwOnUndefined) { this._emit(`,${node.lineno},${node.colno})`); } this._emit(', env.opts.autoescape);\n'); } }); } compileRoot(node, frame) { if (frame) { this.fail('compileRoot: root node can\'t have frame'); } frame = new Frame(); this._emitFuncBegin(node, 'root'); this._emitLine('var parentTemplate = null;'); this._compileChildren(node, frame); this._emitLine('if(parentTemplate) {'); this._emitLine('parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);'); this._emitLine('} else {'); this._emitLine(`cb(null, ${this.buffer});`); this._emitLine('}'); this._emitFuncEnd(true); this.inBlock = true; const blockNames = []; const blocks = node.findAll(nodes.Block); blocks.forEach((block, i) => { const name = block.name.value; if (blockNames.indexOf(name) !== -1) { throw new Error(`Block "${name}" defined more than once.`); } blockNames.push(name); this._emitFuncBegin(block, `b_${name}`); const tmpFrame = new Frame(); this._emitLine('var frame = frame.push(true);'); this.compile(block.body, tmpFrame); this._emitFuncEnd(); }); this._emitLine('return {'); blocks.forEach((block, i) => { const blockName = `b_${block.name.value}`; this._emitLine(`${blockName}: ${blockName},`); }); this._emitLine('root: root\n};'); } compile(node, frame) { var _compile = this['compile' + node.typename]; if (_compile) { _compile.call(this, node, frame); } else { this.fail(`compile: Cannot compile node: ${node.typename}`, node.lineno, node.colno); } } getCode() { return this.codebuf.join(''); } } module.exports = { compile: function compile(src, asyncFilters, extensions, name, opts = {}) { const c = new Compiler(name, opts.throwOnUndefined); // Run the extension preprocessors against the source. const preprocessors = (extensions || []).map(ext => ext.preprocess).filter(f => !!f); const processedSrc = preprocessors.reduce((s, processor) => processor(s), src); c.compile(transformer.transform( parser.parse(processedSrc, extensions, opts), asyncFilters, name )); return c.getCode(); }, Compiler: Compiler }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/environment.js000664 000000 000000 00000034167 14012546311 025417 0ustar00rootroot000000 000000 'use strict'; const asap = require('asap'); const waterfall = require('a-sync-waterfall'); const lib = require('./lib'); const compiler = require('./compiler'); const filters = require('./filters'); const {FileSystemLoader, WebLoader, PrecompiledLoader} = require('./loaders'); const tests = require('./tests'); const globals = require('./globals'); const {Obj, EmitterObj} = require('./object'); const globalRuntime = require('./runtime'); const {handleError, Frame} = globalRuntime; const expressApp = require('./express-app'); // If the user is using the async API, *always* call it // asynchronously even if the template was synchronous. function callbackAsap(cb, err, res) { asap(() => { cb(err, res); }); } /** * A no-op template, for use with {% include ignore missing %} */ const noopTmplSrc = { type: 'code', obj: { root(env, context, frame, runtime, cb) { try { cb(null, ''); } catch (e) { cb(handleError(e, null, null)); } } } }; class Environment extends EmitterObj { init(loaders, opts) { // The dev flag determines the trace that'll be shown on errors. // If set to true, returns the full trace from the error point, // otherwise will return trace starting from Template.render // (the full trace from within nunjucks may confuse developers using // the library) // defaults to false opts = this.opts = opts || {}; this.opts.dev = !!opts.dev; // The autoescape flag sets global autoescaping. If true, // every string variable will be escaped by default. // If false, strings can be manually escaped using the `escape` filter. // defaults to true this.opts.autoescape = opts.autoescape != null ? opts.autoescape : true; // If true, this will make the system throw errors if trying // to output a null or undefined value this.opts.throwOnUndefined = !!opts.throwOnUndefined; this.opts.trimBlocks = !!opts.trimBlocks; this.opts.lstripBlocks = !!opts.lstripBlocks; this.loaders = []; if (!loaders) { // The filesystem loader is only available server-side if (FileSystemLoader) { this.loaders = [new FileSystemLoader('views')]; } else if (WebLoader) { this.loaders = [new WebLoader('/views')]; } } else { this.loaders = lib.isArray(loaders) ? loaders : [loaders]; } // It's easy to use precompiled templates: just include them // before you configure nunjucks and this will automatically // pick it up and use it if (typeof window !== 'undefined' && window.nunjucksPrecompiled) { this.loaders.unshift( new PrecompiledLoader(window.nunjucksPrecompiled) ); } this._initLoaders(); this.globals = globals(); this.filters = {}; this.tests = {}; this.asyncFilters = []; this.extensions = {}; this.extensionsList = []; lib._entries(filters).forEach(([name, filter]) => this.addFilter(name, filter)); lib._entries(tests).forEach(([name, test]) => this.addTest(name, test)); } _initLoaders() { this.loaders.forEach((loader) => { // Caching and cache busting loader.cache = {}; if (typeof loader.on === 'function') { loader.on('update', (name, fullname) => { loader.cache[name] = null; this.emit('update', name, fullname, loader); }); loader.on('load', (name, source) => { this.emit('load', name, source, loader); }); } }); } invalidateCache() { this.loaders.forEach((loader) => { loader.cache = {}; }); } addExtension(name, extension) { extension.__name = name; this.extensions[name] = extension; this.extensionsList.push(extension); return this; } removeExtension(name) { var extension = this.getExtension(name); if (!extension) { return; } this.extensionsList = lib.without(this.extensionsList, extension); delete this.extensions[name]; } getExtension(name) { return this.extensions[name]; } hasExtension(name) { return !!this.extensions[name]; } addGlobal(name, value) { this.globals[name] = value; return this; } getGlobal(name) { if (typeof this.globals[name] === 'undefined') { throw new Error('global not found: ' + name); } return this.globals[name]; } addFilter(name, func, async) { var wrapped = func; if (async) { this.asyncFilters.push(name); } this.filters[name] = wrapped; return this; } getFilter(name) { if (!this.filters[name]) { throw new Error('filter not found: ' + name); } return this.filters[name]; } addTest(name, func) { this.tests[name] = func; return this; } getTest(name) { if (!this.tests[name]) { throw new Error('test not found: ' + name); } return this.tests[name]; } resolveTemplate(loader, parentName, filename) { var isRelative = (loader.isRelative && parentName) ? loader.isRelative(filename) : false; return (isRelative && loader.resolve) ? loader.resolve(parentName, filename) : filename; } getTemplate(name, eagerCompile, parentName, ignoreMissing, cb) { var that = this; var tmpl = null; if (name && name.raw) { // this fixes autoescape for templates referenced in symbols name = name.raw; } if (lib.isFunction(parentName)) { cb = parentName; parentName = null; eagerCompile = eagerCompile || false; } if (lib.isFunction(eagerCompile)) { cb = eagerCompile; eagerCompile = false; } if (name instanceof Template) { tmpl = name; } else if (typeof name !== 'string') { throw new Error('template names must be a string: ' + name); } else { for (let i = 0; i < this.loaders.length; i++) { const loader = this.loaders[i]; tmpl = loader.cache[this.resolveTemplate(loader, parentName, name)]; if (tmpl) { break; } } } if (tmpl) { if (eagerCompile) { tmpl.compile(); } if (cb) { cb(null, tmpl); return undefined; } else { return tmpl; } } let syncResult; const createTemplate = (err, info) => { if (!info && !err && !ignoreMissing) { err = new Error('template not found: ' + name); } if (err) { if (cb) { cb(err); return; } else { throw err; } } let newTmpl; if (!info) { newTmpl = new Template(noopTmplSrc, this, '', eagerCompile); } else { newTmpl = new Template(info.src, this, info.path, eagerCompile); if (!info.noCache) { info.loader.cache[name] = newTmpl; } } if (cb) { cb(null, newTmpl); } else { syncResult = newTmpl; } }; lib.asyncIter(this.loaders, (loader, i, next, done) => { function handle(err, src) { if (err) { done(err); } else if (src) { src.loader = loader; done(null, src); } else { next(); } } // Resolve name relative to parentName name = that.resolveTemplate(loader, parentName, name); if (loader.async) { loader.getSource(name, handle); } else { handle(null, loader.getSource(name)); } }, createTemplate); return syncResult; } express(app) { return expressApp(this, app); } render(name, ctx, cb) { if (lib.isFunction(ctx)) { cb = ctx; ctx = null; } // We support a synchronous API to make it easier to migrate // existing code to async. This works because if you don't do // anything async work, the whole thing is actually run // synchronously. let syncResult = null; this.getTemplate(name, (err, tmpl) => { if (err && cb) { callbackAsap(cb, err); } else if (err) { throw err; } else { syncResult = tmpl.render(ctx, cb); } }); return syncResult; } renderString(src, ctx, opts, cb) { if (lib.isFunction(opts)) { cb = opts; opts = {}; } opts = opts || {}; const tmpl = new Template(src, this, opts.path); return tmpl.render(ctx, cb); } waterfall(tasks, callback, forceAsync) { return waterfall(tasks, callback, forceAsync); } } class Context extends Obj { init(ctx, blocks, env) { // Has to be tied to an environment so we can tap into its globals. this.env = env || new Environment(); // Make a duplicate of ctx this.ctx = lib.extend({}, ctx); this.blocks = {}; this.exported = []; lib.keys(blocks).forEach(name => { this.addBlock(name, blocks[name]); }); } lookup(name) { // This is one of the most called functions, so optimize for // the typical case where the name isn't in the globals if (name in this.env.globals && !(name in this.ctx)) { return this.env.globals[name]; } else { return this.ctx[name]; } } setVariable(name, val) { this.ctx[name] = val; } getVariables() { return this.ctx; } addBlock(name, block) { this.blocks[name] = this.blocks[name] || []; this.blocks[name].push(block); return this; } getBlock(name) { if (!this.blocks[name]) { throw new Error('unknown block "' + name + '"'); } return this.blocks[name][0]; } getSuper(env, name, block, frame, runtime, cb) { var idx = lib.indexOf(this.blocks[name] || [], block); var blk = this.blocks[name][idx + 1]; var context = this; if (idx === -1 || !blk) { throw new Error('no super block available for "' + name + '"'); } blk(env, context, frame, runtime, cb); } addExport(name) { this.exported.push(name); } getExported() { var exported = {}; this.exported.forEach((name) => { exported[name] = this.ctx[name]; }); return exported; } } class Template extends Obj { init(src, env, path, eagerCompile) { this.env = env || new Environment(); if (lib.isObject(src)) { switch (src.type) { case 'code': this.tmplProps = src.obj; break; case 'string': this.tmplStr = src.obj; break; default: throw new Error( `Unexpected template object type ${src.type}; expected 'code', or 'string'`); } } else if (lib.isString(src)) { this.tmplStr = src; } else { throw new Error('src must be a string or an object describing the source'); } this.path = path; if (eagerCompile) { try { this._compile(); } catch (err) { throw lib._prettifyError(this.path, this.env.opts.dev, err); } } else { this.compiled = false; } } render(ctx, parentFrame, cb) { if (typeof ctx === 'function') { cb = ctx; ctx = {}; } else if (typeof parentFrame === 'function') { cb = parentFrame; parentFrame = null; } // If there is a parent frame, we are being called from internal // code of another template, and the internal system // depends on the sync/async nature of the parent template // to be inherited, so force an async callback const forceAsync = !parentFrame; // Catch compile errors for async rendering try { this.compile(); } catch (e) { const err = lib._prettifyError(this.path, this.env.opts.dev, e); if (cb) { return callbackAsap(cb, err); } else { throw err; } } const context = new Context(ctx || {}, this.blocks, this.env); const frame = parentFrame ? parentFrame.push(true) : new Frame(); frame.topLevel = true; let syncResult = null; let didError = false; this.rootRenderFunc(this.env, context, frame, globalRuntime, (err, res) => { // TODO: this is actually a bug in the compiled template (because waterfall // tasks are both not passing errors up the chain of callbacks AND are not // causing a return from the top-most render function). But fixing that // will require a more substantial change to the compiler. if (didError && cb && typeof res !== 'undefined') { // prevent multiple calls to cb return; } if (err) { err = lib._prettifyError(this.path, this.env.opts.dev, err); didError = true; } if (cb) { if (forceAsync) { callbackAsap(cb, err, res); } else { cb(err, res); } } else { if (err) { throw err; } syncResult = res; } }); return syncResult; } getExported(ctx, parentFrame, cb) { // eslint-disable-line consistent-return if (typeof ctx === 'function') { cb = ctx; ctx = {}; } if (typeof parentFrame === 'function') { cb = parentFrame; parentFrame = null; } // Catch compile errors for async rendering try { this.compile(); } catch (e) { if (cb) { return cb(e); } else { throw e; } } const frame = parentFrame ? parentFrame.push() : new Frame(); frame.topLevel = true; // Run the rootRenderFunc to populate the context with exported vars const context = new Context(ctx || {}, this.blocks, this.env); this.rootRenderFunc(this.env, context, frame, globalRuntime, (err) => { if (err) { cb(err, null); } else { cb(null, context.getExported()); } }); } compile() { if (!this.compiled) { this._compile(); } } _compile() { var props; if (this.tmplProps) { props = this.tmplProps; } else { const source = compiler.compile(this.tmplStr, this.env.asyncFilters, this.env.extensionsList, this.path, this.env.opts); const func = new Function(source); // eslint-disable-line no-new-func props = func(); } this.blocks = this._getBlocks(props); this.rootRenderFunc = props.root; this.compiled = true; } _getBlocks(props) { var blocks = {}; lib.keys(props).forEach((k) => { if (k.slice(0, 2) === 'b_') { blocks[k.slice(2)] = props[k]; } }); return blocks; } } module.exports = { Environment: Environment, Template: Template }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/express-app.js000664 000000 000000 00000001265 14012546311 025313 0ustar00rootroot000000 000000 const path = require('path'); module.exports = function express(env, app) { function NunjucksView(name, opts) { this.name = name; this.path = name; this.defaultEngine = opts.defaultEngine; this.ext = path.extname(name); if (!this.ext && !this.defaultEngine) { throw new Error('No default engine was specified and no extension was provided.'); } if (!this.ext) { this.name += (this.ext = (this.defaultEngine[0] !== '.' ? '.' : '') + this.defaultEngine); } } NunjucksView.prototype.render = function render(opts, cb) { env.render(this.name, opts, cb); }; app.set('view', NunjucksView); app.set('nunjucksEnv', env); return env; }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/filters.js000664 000000 000000 00000034336 14012546311 024521 0ustar00rootroot000000 000000 'use strict'; var lib = require('./lib'); var r = require('./runtime'); var exports = module.exports = {}; function normalize(value, defaultValue) { if (value === null || value === undefined || value === false) { return defaultValue; } return value; } exports.abs = Math.abs; function isNaN(num) { return num !== num; // eslint-disable-line no-self-compare } function batch(arr, linecount, fillWith) { var i; var res = []; var tmp = []; for (i = 0; i < arr.length; i++) { if (i % linecount === 0 && tmp.length) { res.push(tmp); tmp = []; } tmp.push(arr[i]); } if (tmp.length) { if (fillWith) { for (i = tmp.length; i < linecount; i++) { tmp.push(fillWith); } } res.push(tmp); } return res; } exports.batch = batch; function capitalize(str) { str = normalize(str, ''); const ret = str.toLowerCase(); return r.copySafeness(str, ret.charAt(0).toUpperCase() + ret.slice(1)); } exports.capitalize = capitalize; function center(str, width) { str = normalize(str, ''); width = width || 80; if (str.length >= width) { return str; } const spaces = width - str.length; const pre = lib.repeat(' ', (spaces / 2) - (spaces % 2)); const post = lib.repeat(' ', spaces / 2); return r.copySafeness(str, pre + str + post); } exports.center = center; function default_(val, def, bool) { if (bool) { return val || def; } else { return (val !== undefined) ? val : def; } } // TODO: it is confusing to export something called 'default' exports['default'] = default_; // eslint-disable-line dot-notation function dictsort(val, caseSensitive, by) { if (!lib.isObject(val)) { throw new lib.TemplateError('dictsort filter: val must be an object'); } let array = []; // deliberately include properties from the object's prototype for (let k in val) { // eslint-disable-line guard-for-in, no-restricted-syntax array.push([k, val[k]]); } let si; if (by === undefined || by === 'key') { si = 0; } else if (by === 'value') { si = 1; } else { throw new lib.TemplateError( 'dictsort filter: You can only sort by either key or value'); } array.sort((t1, t2) => { var a = t1[si]; var b = t2[si]; if (!caseSensitive) { if (lib.isString(a)) { a = a.toUpperCase(); } if (lib.isString(b)) { b = b.toUpperCase(); } } return a > b ? 1 : (a === b ? 0 : -1); // eslint-disable-line no-nested-ternary }); return array; } exports.dictsort = dictsort; function dump(obj, spaces) { return JSON.stringify(obj, null, spaces); } exports.dump = dump; function escape(str) { if (str instanceof r.SafeString) { return str; } str = (str === null || str === undefined) ? '' : str; return r.markSafe(lib.escape(str.toString())); } exports.escape = escape; function safe(str) { if (str instanceof r.SafeString) { return str; } str = (str === null || str === undefined) ? '' : str; return r.markSafe(str.toString()); } exports.safe = safe; function first(arr) { return arr[0]; } exports.first = first; function forceescape(str) { str = (str === null || str === undefined) ? '' : str; return r.markSafe(lib.escape(str.toString())); } exports.forceescape = forceescape; function groupby(arr, attr) { return lib.groupBy(arr, attr, this.env.opts.throwOnUndefined); } exports.groupby = groupby; function indent(str, width, indentfirst) { str = normalize(str, ''); if (str === '') { return ''; } width = width || 4; // let res = ''; const lines = str.split('\n'); const sp = lib.repeat(' ', width); const res = lines.map((l, i) => { return (i === 0 && !indentfirst) ? l : `${sp}${l}`; }).join('\n'); return r.copySafeness(str, res); } exports.indent = indent; function join(arr, del, attr) { del = del || ''; if (attr) { arr = lib.map(arr, (v) => v[attr]); } return arr.join(del); } exports.join = join; function last(arr) { return arr[arr.length - 1]; } exports.last = last; function lengthFilter(val) { var value = normalize(val, ''); if (value !== undefined) { if ( (typeof Map === 'function' && value instanceof Map) || (typeof Set === 'function' && value instanceof Set) ) { // ECMAScript 2015 Maps and Sets return value.size; } if (lib.isObject(value) && !(value instanceof r.SafeString)) { // Objects (besides SafeStrings), non-primative Arrays return lib.keys(value).length; } return value.length; } return 0; } exports.length = lengthFilter; function list(val) { if (lib.isString(val)) { return val.split(''); } else if (lib.isObject(val)) { return lib._entries(val || {}).map(([key, value]) => ({key, value})); } else if (lib.isArray(val)) { return val; } else { throw new lib.TemplateError('list filter: type not iterable'); } } exports.list = list; function lower(str) { str = normalize(str, ''); return str.toLowerCase(); } exports.lower = lower; function nl2br(str) { if (str === null || str === undefined) { return ''; } return r.copySafeness(str, str.replace(/\r\n|\n/g, '
\n')); } exports.nl2br = nl2br; function random(arr) { return arr[Math.floor(Math.random() * arr.length)]; } exports.random = random; /** * Construct select or reject filter * * @param {boolean} expectedTestResult * @returns {function(array, string, *): array} */ function getSelectOrReject(expectedTestResult) { function filter(arr, testName = 'truthy', secondArg) { const context = this; const test = context.env.getTest(testName); return lib.toArray(arr).filter(function examineTestResult(item) { return test.call(context, item, secondArg) === expectedTestResult; }); } return filter; } exports.reject = getSelectOrReject(false); function rejectattr(arr, attr) { return arr.filter((item) => !item[attr]); } exports.rejectattr = rejectattr; exports.select = getSelectOrReject(true); function selectattr(arr, attr) { return arr.filter((item) => !!item[attr]); } exports.selectattr = selectattr; function replace(str, old, new_, maxCount) { var originalStr = str; if (old instanceof RegExp) { return str.replace(old, new_); } if (typeof maxCount === 'undefined') { maxCount = -1; } let res = ''; // Output // Cast Numbers in the search term to string if (typeof old === 'number') { old = '' + old; } else if (typeof old !== 'string') { // If it is something other than number or string, // return the original string return str; } // Cast numbers in the replacement to string if (typeof str === 'number') { str = '' + str; } // If by now, we don't have a string, throw it back if (typeof str !== 'string' && !(str instanceof r.SafeString)) { return str; } // ShortCircuits if (old === '') { // Mimic the python behaviour: empty string is replaced // by replacement e.g. "abc"|replace("", ".") -> .a.b.c. res = new_ + str.split('').join(new_) + new_; return r.copySafeness(str, res); } let nextIndex = str.indexOf(old); // if # of replacements to perform is 0, or the string to does // not contain the old value, return the string if (maxCount === 0 || nextIndex === -1) { return str; } let pos = 0; let count = 0; // # of replacements made while (nextIndex > -1 && (maxCount === -1 || count < maxCount)) { // Grab the next chunk of src string and add it with the // replacement, to the result res += str.substring(pos, nextIndex) + new_; // Increment our pointer in the src string pos = nextIndex + old.length; count++; // See if there are any more replacements to be made nextIndex = str.indexOf(old, pos); } // We've either reached the end, or done the max # of // replacements, tack on any remaining string if (pos < str.length) { res += str.substring(pos); } return r.copySafeness(originalStr, res); } exports.replace = replace; function reverse(val) { var arr; if (lib.isString(val)) { arr = list(val); } else { // Copy it arr = lib.map(val, v => v); } arr.reverse(); if (lib.isString(val)) { return r.copySafeness(val, arr.join('')); } return arr; } exports.reverse = reverse; function round(val, precision, method) { precision = precision || 0; const factor = Math.pow(10, precision); let rounder; if (method === 'ceil') { rounder = Math.ceil; } else if (method === 'floor') { rounder = Math.floor; } else { rounder = Math.round; } return rounder(val * factor) / factor; } exports.round = round; function slice(arr, slices, fillWith) { const sliceLength = Math.floor(arr.length / slices); const extra = arr.length % slices; const res = []; let offset = 0; for (let i = 0; i < slices; i++) { const start = offset + (i * sliceLength); if (i < extra) { offset++; } const end = offset + ((i + 1) * sliceLength); const currSlice = arr.slice(start, end); if (fillWith && i >= extra) { currSlice.push(fillWith); } res.push(currSlice); } return res; } exports.slice = slice; function sum(arr, attr, start = 0) { if (attr) { arr = lib.map(arr, (v) => v[attr]); } return start + arr.reduce((a, b) => a + b, 0); } exports.sum = sum; exports.sort = r.makeMacro( ['value', 'reverse', 'case_sensitive', 'attribute'], [], function sortFilter(arr, reversed, caseSens, attr) { // Copy it let array = lib.map(arr, v => v); let getAttribute = lib.getAttrGetter(attr); array.sort((a, b) => { let x = (attr) ? getAttribute(a) : a; let y = (attr) ? getAttribute(b) : b; if ( this.env.opts.throwOnUndefined && attr && (x === undefined || y === undefined) ) { throw new TypeError(`sort: attribute "${attr}" resolved to undefined`); } if (!caseSens && lib.isString(x) && lib.isString(y)) { x = x.toLowerCase(); y = y.toLowerCase(); } if (x < y) { return reversed ? 1 : -1; } else if (x > y) { return reversed ? -1 : 1; } else { return 0; } }); return array; }); function string(obj) { return r.copySafeness(obj, obj); } exports.string = string; function striptags(input, preserveLinebreaks) { input = normalize(input, ''); let tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi; let trimmedInput = trim(input.replace(tags, '')); let res = ''; if (preserveLinebreaks) { res = trimmedInput .replace(/^ +| +$/gm, '') // remove leading and trailing spaces .replace(/ +/g, ' ') // squash adjacent spaces .replace(/(\r\n)/g, '\n') // normalize linebreaks (CRLF -> LF) .replace(/\n\n\n+/g, '\n\n'); // squash abnormal adjacent linebreaks } else { res = trimmedInput.replace(/\s+/gi, ' '); } return r.copySafeness(input, res); } exports.striptags = striptags; function title(str) { str = normalize(str, ''); let words = str.split(' ').map(word => capitalize(word)); return r.copySafeness(str, words.join(' ')); } exports.title = title; function trim(str) { return r.copySafeness(str, str.replace(/^\s*|\s*$/g, '')); } exports.trim = trim; function truncate(input, length, killwords, end) { var orig = input; input = normalize(input, ''); length = length || 255; if (input.length <= length) { return input; } if (killwords) { input = input.substring(0, length); } else { let idx = input.lastIndexOf(' ', length); if (idx === -1) { idx = length; } input = input.substring(0, idx); } input += (end !== undefined && end !== null) ? end : '...'; return r.copySafeness(orig, input); } exports.truncate = truncate; function upper(str) { str = normalize(str, ''); return str.toUpperCase(); } exports.upper = upper; function urlencode(obj) { var enc = encodeURIComponent; if (lib.isString(obj)) { return enc(obj); } else { let keyvals = (lib.isArray(obj)) ? obj : lib._entries(obj); return keyvals.map(([k, v]) => `${enc(k)}=${enc(v)}`).join('&'); } } exports.urlencode = urlencode; // For the jinja regexp, see // https://github.com/mitsuhiko/jinja2/blob/f15b814dcba6aa12bc74d1f7d0c881d55f7126be/jinja2/utils.py#L20-L23 const puncRe = /^(?:\(|<|<)?(.*?)(?:\.|,|\)|\n|>)?$/; // from http://blog.gerv.net/2011/05/html5_email_address_regexp/ const emailRe = /^[\w.!#$%&'*+\-\/=?\^`{|}~]+@[a-z\d\-]+(\.[a-z\d\-]+)+$/i; const httpHttpsRe = /^https?:\/\/.*$/; const wwwRe = /^www\./; const tldRe = /\.(?:org|net|com)(?:\:|\/|$)/; function urlize(str, length, nofollow) { if (isNaN(length)) { length = Infinity; } const noFollowAttr = (nofollow === true ? ' rel="nofollow"' : ''); const words = str.split(/(\s+)/).filter((word) => { // If the word has no length, bail. This can happen for str with // trailing whitespace. return word && word.length; }).map((word) => { var matches = word.match(puncRe); var possibleUrl = (matches) ? matches[1] : word; var shortUrl = possibleUrl.substr(0, length); // url that starts with http or https if (httpHttpsRe.test(possibleUrl)) { return `${shortUrl}`; } // url that starts with www. if (wwwRe.test(possibleUrl)) { return `${shortUrl}`; } // an email address of the form username@domain.tld if (emailRe.test(possibleUrl)) { return `${possibleUrl}`; } // url that ends in .com, .org or .net that is not an email address if (tldRe.test(possibleUrl)) { return `${shortUrl}`; } return word; }); return words.join(''); } exports.urlize = urlize; function wordcount(str) { str = normalize(str, ''); const words = (str) ? str.match(/\w+/g) : null; return (words) ? words.length : null; } exports.wordcount = wordcount; function float(val, def) { var res = parseFloat(val); return (isNaN(res)) ? def : res; } exports.float = float; const intFilter = r.makeMacro( ['value', 'default', 'base'], [], function doInt(value, defaultValue, base = 10) { var res = parseInt(value, base); return (isNaN(res)) ? defaultValue : res; } ); exports.int = intFilter; // Aliases exports.d = exports.default; exports.e = exports.escape; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/globals.js000664 000000 000000 00000002475 14012546311 024473 0ustar00rootroot000000 000000 'use strict'; function cycler(items) { var index = -1; return { current: null, reset() { index = -1; this.current = null; }, next() { index++; if (index >= items.length) { index = 0; } this.current = items[index]; return this.current; }, }; } function joiner(sep) { sep = sep || ','; let first = true; return () => { const val = first ? '' : sep; first = false; return val; }; } // Making this a function instead so it returns a new object // each time it's called. That way, if something like an environment // uses it, they will each have their own copy. function globals() { return { range(start, stop, step) { if (typeof stop === 'undefined') { stop = start; start = 0; step = 1; } else if (!step) { step = 1; } const arr = []; if (step > 0) { for (let i = start; i < stop; i += step) { arr.push(i); } } else { for (let i = start; i > stop; i += step) { // eslint-disable-line for-direction arr.push(i); } } return arr; }, cycler() { return cycler(Array.prototype.slice.call(arguments)); }, joiner(sep) { return joiner(sep); } }; } module.exports = globals; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/jinja-compat.js000664 000000 000000 00000017636 14012546311 025431 0ustar00rootroot000000 000000 function installCompat() { 'use strict'; /* eslint-disable camelcase */ // This must be called like `nunjucks.installCompat` so that `this` // references the nunjucks instance var runtime = this.runtime; var lib = this.lib; // Handle slim case where these 'modules' are excluded from the built source var Compiler = this.compiler.Compiler; var Parser = this.parser.Parser; var nodes = this.nodes; var lexer = this.lexer; var orig_contextOrFrameLookup = runtime.contextOrFrameLookup; var orig_memberLookup = runtime.memberLookup; var orig_Compiler_assertType; var orig_Parser_parseAggregate; if (Compiler) { orig_Compiler_assertType = Compiler.prototype.assertType; } if (Parser) { orig_Parser_parseAggregate = Parser.prototype.parseAggregate; } function uninstall() { runtime.contextOrFrameLookup = orig_contextOrFrameLookup; runtime.memberLookup = orig_memberLookup; if (Compiler) { Compiler.prototype.assertType = orig_Compiler_assertType; } if (Parser) { Parser.prototype.parseAggregate = orig_Parser_parseAggregate; } } runtime.contextOrFrameLookup = function contextOrFrameLookup(context, frame, key) { var val = orig_contextOrFrameLookup.apply(this, arguments); if (val !== undefined) { return val; } switch (key) { case 'True': return true; case 'False': return false; case 'None': return null; default: return undefined; } }; function getTokensState(tokens) { return { index: tokens.index, lineno: tokens.lineno, colno: tokens.colno }; } if (process.env.BUILD_TYPE !== 'SLIM' && nodes && Compiler && Parser) { // i.e., not slim mode const Slice = nodes.Node.extend('Slice', { fields: ['start', 'stop', 'step'], init(lineno, colno, start, stop, step) { start = start || new nodes.Literal(lineno, colno, null); stop = stop || new nodes.Literal(lineno, colno, null); step = step || new nodes.Literal(lineno, colno, 1); this.parent(lineno, colno, start, stop, step); } }); Compiler.prototype.assertType = function assertType(node) { if (node instanceof Slice) { return; } orig_Compiler_assertType.apply(this, arguments); }; Compiler.prototype.compileSlice = function compileSlice(node, frame) { this._emit('('); this._compileExpression(node.start, frame); this._emit('),('); this._compileExpression(node.stop, frame); this._emit('),('); this._compileExpression(node.step, frame); this._emit(')'); }; Parser.prototype.parseAggregate = function parseAggregate() { var origState = getTokensState(this.tokens); // Set back one accounting for opening bracket/parens origState.colno--; origState.index--; try { return orig_Parser_parseAggregate.apply(this); } catch (e) { const errState = getTokensState(this.tokens); const rethrow = () => { lib._assign(this.tokens, errState); return e; }; // Reset to state before original parseAggregate called lib._assign(this.tokens, origState); this.peeked = false; const tok = this.peekToken(); if (tok.type !== lexer.TOKEN_LEFT_BRACKET) { throw rethrow(); } else { this.nextToken(); } const node = new Slice(tok.lineno, tok.colno); // If we don't encounter a colon while parsing, this is not a slice, // so re-raise the original exception. let isSlice = false; for (let i = 0; i <= node.fields.length; i++) { if (this.skip(lexer.TOKEN_RIGHT_BRACKET)) { break; } if (i === node.fields.length) { if (isSlice) { this.fail('parseSlice: too many slice components', tok.lineno, tok.colno); } else { break; } } if (this.skip(lexer.TOKEN_COLON)) { isSlice = true; } else { const field = node.fields[i]; node[field] = this.parseExpression(); isSlice = this.skip(lexer.TOKEN_COLON) || isSlice; } } if (!isSlice) { throw rethrow(); } return new nodes.Array(tok.lineno, tok.colno, [node]); } }; } function sliceLookup(obj, start, stop, step) { obj = obj || []; if (start === null) { start = (step < 0) ? (obj.length - 1) : 0; } if (stop === null) { stop = (step < 0) ? -1 : obj.length; } else if (stop < 0) { stop += obj.length; } if (start < 0) { start += obj.length; } const results = []; for (let i = start; ; i += step) { if (i < 0 || i > obj.length) { break; } if (step > 0 && i >= stop) { break; } if (step < 0 && i <= stop) { break; } results.push(runtime.memberLookup(obj, i)); } return results; } function hasOwnProp(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } const ARRAY_MEMBERS = { pop(index) { if (index === undefined) { return this.pop(); } if (index >= this.length || index < 0) { throw new Error('KeyError'); } return this.splice(index, 1); }, append(element) { return this.push(element); }, remove(element) { for (let i = 0; i < this.length; i++) { if (this[i] === element) { return this.splice(i, 1); } } throw new Error('ValueError'); }, count(element) { var count = 0; for (let i = 0; i < this.length; i++) { if (this[i] === element) { count++; } } return count; }, index(element) { var i; if ((i = this.indexOf(element)) === -1) { throw new Error('ValueError'); } return i; }, find(element) { return this.indexOf(element); }, insert(index, elem) { return this.splice(index, 0, elem); } }; const OBJECT_MEMBERS = { items() { return lib._entries(this); }, values() { return lib._values(this); }, keys() { return lib.keys(this); }, get(key, def) { var output = this[key]; if (output === undefined) { output = def; } return output; }, has_key(key) { return hasOwnProp(this, key); }, pop(key, def) { var output = this[key]; if (output === undefined && def !== undefined) { output = def; } else if (output === undefined) { throw new Error('KeyError'); } else { delete this[key]; } return output; }, popitem() { const keys = lib.keys(this); if (!keys.length) { throw new Error('KeyError'); } const k = keys[0]; const val = this[k]; delete this[k]; return [k, val]; }, setdefault(key, def = null) { if (!(key in this)) { this[key] = def; } return this[key]; }, update(kwargs) { lib._assign(this, kwargs); return null; // Always returns None } }; OBJECT_MEMBERS.iteritems = OBJECT_MEMBERS.items; OBJECT_MEMBERS.itervalues = OBJECT_MEMBERS.values; OBJECT_MEMBERS.iterkeys = OBJECT_MEMBERS.keys; runtime.memberLookup = function memberLookup(obj, val, autoescape) { if (arguments.length === 4) { return sliceLookup.apply(this, arguments); } obj = obj || {}; // If the object is an object, return any of the methods that Python would // otherwise provide. if (lib.isArray(obj) && hasOwnProp(ARRAY_MEMBERS, val)) { return ARRAY_MEMBERS[val].bind(obj); } if (lib.isObject(obj) && hasOwnProp(OBJECT_MEMBERS, val)) { return OBJECT_MEMBERS[val].bind(obj); } return orig_memberLookup.apply(this, arguments); }; return uninstall; } module.exports = installCompat; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/lexer.js000664 000000 000000 00000036145 14012546311 024170 0ustar00rootroot000000 000000 'use strict'; const lib = require('./lib'); let whitespaceChars = ' \n\t\r\u00A0'; let delimChars = '()[]{}%*-+~/#,:|.<>=!'; let intChars = '0123456789'; let BLOCK_START = '{%'; let BLOCK_END = '%}'; let VARIABLE_START = '{{'; let VARIABLE_END = '}}'; let COMMENT_START = '{#'; let COMMENT_END = '#}'; let TOKEN_STRING = 'string'; let TOKEN_WHITESPACE = 'whitespace'; let TOKEN_DATA = 'data'; let TOKEN_BLOCK_START = 'block-start'; let TOKEN_BLOCK_END = 'block-end'; let TOKEN_VARIABLE_START = 'variable-start'; let TOKEN_VARIABLE_END = 'variable-end'; let TOKEN_COMMENT = 'comment'; let TOKEN_LEFT_PAREN = 'left-paren'; let TOKEN_RIGHT_PAREN = 'right-paren'; let TOKEN_LEFT_BRACKET = 'left-bracket'; let TOKEN_RIGHT_BRACKET = 'right-bracket'; let TOKEN_LEFT_CURLY = 'left-curly'; let TOKEN_RIGHT_CURLY = 'right-curly'; let TOKEN_OPERATOR = 'operator'; let TOKEN_COMMA = 'comma'; let TOKEN_COLON = 'colon'; let TOKEN_TILDE = 'tilde'; let TOKEN_PIPE = 'pipe'; let TOKEN_INT = 'int'; let TOKEN_FLOAT = 'float'; let TOKEN_BOOLEAN = 'boolean'; let TOKEN_NONE = 'none'; let TOKEN_SYMBOL = 'symbol'; let TOKEN_SPECIAL = 'special'; let TOKEN_REGEX = 'regex'; function token(type, value, lineno, colno) { return { type: type, value: value, lineno: lineno, colno: colno }; } class Tokenizer { constructor(str, opts) { this.str = str; this.index = 0; this.len = str.length; this.lineno = 0; this.colno = 0; this.in_code = false; opts = opts || {}; let tags = opts.tags || {}; this.tags = { BLOCK_START: tags.blockStart || BLOCK_START, BLOCK_END: tags.blockEnd || BLOCK_END, VARIABLE_START: tags.variableStart || VARIABLE_START, VARIABLE_END: tags.variableEnd || VARIABLE_END, COMMENT_START: tags.commentStart || COMMENT_START, COMMENT_END: tags.commentEnd || COMMENT_END }; this.trimBlocks = !!opts.trimBlocks; this.lstripBlocks = !!opts.lstripBlocks; } nextToken() { let lineno = this.lineno; let colno = this.colno; let tok; if (this.in_code) { // Otherwise, if we are in a block parse it as code let cur = this.current(); if (this.isFinished()) { // We have nothing else to parse return null; } else if (cur === '"' || cur === '\'') { // We've hit a string return token(TOKEN_STRING, this._parseString(cur), lineno, colno); } else if ((tok = this._extract(whitespaceChars))) { // We hit some whitespace return token(TOKEN_WHITESPACE, tok, lineno, colno); } else if ((tok = this._extractString(this.tags.BLOCK_END)) || (tok = this._extractString('-' + this.tags.BLOCK_END))) { // Special check for the block end tag // // It is a requirement that start and end tags are composed of // delimiter characters (%{}[] etc), and our code always // breaks on delimiters so we can assume the token parsing // doesn't consume these elsewhere this.in_code = false; if (this.trimBlocks) { cur = this.current(); if (cur === '\n') { // Skip newline this.forward(); } else if (cur === '\r') { // Skip CRLF newline this.forward(); cur = this.current(); if (cur === '\n') { this.forward(); } else { // Was not a CRLF, so go back this.back(); } } } return token(TOKEN_BLOCK_END, tok, lineno, colno); } else if ((tok = this._extractString(this.tags.VARIABLE_END)) || (tok = this._extractString('-' + this.tags.VARIABLE_END))) { // Special check for variable end tag (see above) this.in_code = false; return token(TOKEN_VARIABLE_END, tok, lineno, colno); } else if (cur === 'r' && this.str.charAt(this.index + 1) === '/') { // Skip past 'r/'. this.forwardN(2); // Extract until the end of the regex -- / ends it, \/ does not. let regexBody = ''; while (!this.isFinished()) { if (this.current() === '/' && this.previous() !== '\\') { this.forward(); break; } else { regexBody += this.current(); this.forward(); } } // Check for flags. // The possible flags are according to https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/RegExp) let POSSIBLE_FLAGS = ['g', 'i', 'm', 'y']; let regexFlags = ''; while (!this.isFinished()) { let isCurrentAFlag = POSSIBLE_FLAGS.indexOf(this.current()) !== -1; if (isCurrentAFlag) { regexFlags += this.current(); this.forward(); } else { break; } } return token(TOKEN_REGEX, { body: regexBody, flags: regexFlags }, lineno, colno); } else if (delimChars.indexOf(cur) !== -1) { // We've hit a delimiter (a special char like a bracket) this.forward(); let complexOps = ['==', '===', '!=', '!==', '<=', '>=', '//', '**']; let curComplex = cur + this.current(); let type; if (lib.indexOf(complexOps, curComplex) !== -1) { this.forward(); cur = curComplex; // See if this is a strict equality/inequality comparator if (lib.indexOf(complexOps, curComplex + this.current()) !== -1) { cur = curComplex + this.current(); this.forward(); } } switch (cur) { case '(': type = TOKEN_LEFT_PAREN; break; case ')': type = TOKEN_RIGHT_PAREN; break; case '[': type = TOKEN_LEFT_BRACKET; break; case ']': type = TOKEN_RIGHT_BRACKET; break; case '{': type = TOKEN_LEFT_CURLY; break; case '}': type = TOKEN_RIGHT_CURLY; break; case ',': type = TOKEN_COMMA; break; case ':': type = TOKEN_COLON; break; case '~': type = TOKEN_TILDE; break; case '|': type = TOKEN_PIPE; break; default: type = TOKEN_OPERATOR; } return token(type, cur, lineno, colno); } else { // We are not at whitespace or a delimiter, so extract the // text and parse it tok = this._extractUntil(whitespaceChars + delimChars); if (tok.match(/^[-+]?[0-9]+$/)) { if (this.current() === '.') { this.forward(); let dec = this._extract(intChars); return token(TOKEN_FLOAT, tok + '.' + dec, lineno, colno); } else { return token(TOKEN_INT, tok, lineno, colno); } } else if (tok.match(/^(true|false)$/)) { return token(TOKEN_BOOLEAN, tok, lineno, colno); } else if (tok === 'none') { return token(TOKEN_NONE, tok, lineno, colno); /* * Added to make the test `null is null` evaluate truthily. * Otherwise, Nunjucks will look up null in the context and * return `undefined`, which is not what we want. This *may* have * consequences is someone is using null in their templates as a * variable. */ } else if (tok === 'null') { return token(TOKEN_NONE, tok, lineno, colno); } else if (tok) { return token(TOKEN_SYMBOL, tok, lineno, colno); } else { throw new Error('Unexpected value while parsing: ' + tok); } } } else { // Parse out the template text, breaking on tag // delimiters because we need to look for block/variable start // tags (don't use the full delimChars for optimization) let beginChars = (this.tags.BLOCK_START.charAt(0) + this.tags.VARIABLE_START.charAt(0) + this.tags.COMMENT_START.charAt(0) + this.tags.COMMENT_END.charAt(0)); if (this.isFinished()) { return null; } else if ((tok = this._extractString(this.tags.BLOCK_START + '-')) || (tok = this._extractString(this.tags.BLOCK_START))) { this.in_code = true; return token(TOKEN_BLOCK_START, tok, lineno, colno); } else if ((tok = this._extractString(this.tags.VARIABLE_START + '-')) || (tok = this._extractString(this.tags.VARIABLE_START))) { this.in_code = true; return token(TOKEN_VARIABLE_START, tok, lineno, colno); } else { tok = ''; let data; let inComment = false; if (this._matches(this.tags.COMMENT_START)) { inComment = true; tok = this._extractString(this.tags.COMMENT_START); } // Continually consume text, breaking on the tag delimiter // characters and checking to see if it's a start tag. // // We could hit the end of the template in the middle of // our looping, so check for the null return value from // _extractUntil while ((data = this._extractUntil(beginChars)) !== null) { tok += data; if ((this._matches(this.tags.BLOCK_START) || this._matches(this.tags.VARIABLE_START) || this._matches(this.tags.COMMENT_START)) && !inComment) { if (this.lstripBlocks && this._matches(this.tags.BLOCK_START) && this.colno > 0 && this.colno <= tok.length) { let lastLine = tok.slice(-this.colno); if (/^\s+$/.test(lastLine)) { // Remove block leading whitespace from beginning of the string tok = tok.slice(0, -this.colno); if (!tok.length) { // All data removed, collapse to avoid unnecessary nodes // by returning next token (block start) return this.nextToken(); } } } // If it is a start tag, stop looping break; } else if (this._matches(this.tags.COMMENT_END)) { if (!inComment) { throw new Error('unexpected end of comment'); } tok += this._extractString(this.tags.COMMENT_END); break; } else { // It does not match any tag, so add the character and // carry on tok += this.current(); this.forward(); } } if (data === null && inComment) { throw new Error('expected end of comment, got end of file'); } return token(inComment ? TOKEN_COMMENT : TOKEN_DATA, tok, lineno, colno); } } } _parseString(delimiter) { this.forward(); let str = ''; while (!this.isFinished() && this.current() !== delimiter) { let cur = this.current(); if (cur === '\\') { this.forward(); switch (this.current()) { case 'n': str += '\n'; break; case 't': str += '\t'; break; case 'r': str += '\r'; break; default: str += this.current(); } this.forward(); } else { str += cur; this.forward(); } } this.forward(); return str; } _matches(str) { if (this.index + str.length > this.len) { return null; } let m = this.str.slice(this.index, this.index + str.length); return m === str; } _extractString(str) { if (this._matches(str)) { this.forwardN(str.length); return str; } return null; } _extractUntil(charString) { // Extract all non-matching chars, with the default matching set // to everything return this._extractMatching(true, charString || ''); } _extract(charString) { // Extract all matching chars (no default, so charString must be // explicit) return this._extractMatching(false, charString); } _extractMatching(breakOnMatch, charString) { // Pull out characters until a breaking char is hit. // If breakOnMatch is false, a non-matching char stops it. // If breakOnMatch is true, a matching char stops it. if (this.isFinished()) { return null; } let first = charString.indexOf(this.current()); // Only proceed if the first character doesn't meet our condition if ((breakOnMatch && first === -1) || (!breakOnMatch && first !== -1)) { let t = this.current(); this.forward(); // And pull out all the chars one at a time until we hit a // breaking char let idx = charString.indexOf(this.current()); while (((breakOnMatch && idx === -1) || (!breakOnMatch && idx !== -1)) && !this.isFinished()) { t += this.current(); this.forward(); idx = charString.indexOf(this.current()); } return t; } return ''; } _extractRegex(regex) { let matches = this.currentStr().match(regex); if (!matches) { return null; } // Move forward whatever was matched this.forwardN(matches[0].length); return matches; } isFinished() { return this.index >= this.len; } forwardN(n) { for (let i = 0; i < n; i++) { this.forward(); } } forward() { this.index++; if (this.previous() === '\n') { this.lineno++; this.colno = 0; } else { this.colno++; } } backN(n) { for (let i = 0; i < n; i++) { this.back(); } } back() { this.index--; if (this.current() === '\n') { this.lineno--; let idx = this.src.lastIndexOf('\n', this.index - 1); if (idx === -1) { this.colno = this.index; } else { this.colno = this.index - idx; } } else { this.colno--; } } // current returns current character current() { if (!this.isFinished()) { return this.str.charAt(this.index); } return ''; } // currentStr returns what's left of the unparsed string currentStr() { if (!this.isFinished()) { return this.str.substr(this.index); } return ''; } previous() { return this.str.charAt(this.index - 1); } } module.exports = { lex(src, opts) { return new Tokenizer(src, opts); }, TOKEN_STRING: TOKEN_STRING, TOKEN_WHITESPACE: TOKEN_WHITESPACE, TOKEN_DATA: TOKEN_DATA, TOKEN_BLOCK_START: TOKEN_BLOCK_START, TOKEN_BLOCK_END: TOKEN_BLOCK_END, TOKEN_VARIABLE_START: TOKEN_VARIABLE_START, TOKEN_VARIABLE_END: TOKEN_VARIABLE_END, TOKEN_COMMENT: TOKEN_COMMENT, TOKEN_LEFT_PAREN: TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN: TOKEN_RIGHT_PAREN, TOKEN_LEFT_BRACKET: TOKEN_LEFT_BRACKET, TOKEN_RIGHT_BRACKET: TOKEN_RIGHT_BRACKET, TOKEN_LEFT_CURLY: TOKEN_LEFT_CURLY, TOKEN_RIGHT_CURLY: TOKEN_RIGHT_CURLY, TOKEN_OPERATOR: TOKEN_OPERATOR, TOKEN_COMMA: TOKEN_COMMA, TOKEN_COLON: TOKEN_COLON, TOKEN_TILDE: TOKEN_TILDE, TOKEN_PIPE: TOKEN_PIPE, TOKEN_INT: TOKEN_INT, TOKEN_FLOAT: TOKEN_FLOAT, TOKEN_BOOLEAN: TOKEN_BOOLEAN, TOKEN_NONE: TOKEN_NONE, TOKEN_SYMBOL: TOKEN_SYMBOL, TOKEN_SPECIAL: TOKEN_SPECIAL, TOKEN_REGEX: TOKEN_REGEX }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/lib.js000664 000000 000000 00000017056 14012546311 023617 0ustar00rootroot000000 000000 'use strict'; var ArrayProto = Array.prototype; var ObjProto = Object.prototype; var escapeMap = { '&': '&', '"': '"', '\'': ''', '<': '<', '>': '>' }; var escapeRegex = /[&"'<>]/g; var exports = module.exports = {}; function hasOwnProp(obj, k) { return ObjProto.hasOwnProperty.call(obj, k); } exports.hasOwnProp = hasOwnProp; function lookupEscape(ch) { return escapeMap[ch]; } function _prettifyError(path, withInternals, err) { if (!err.Update) { // not one of ours, cast it err = new exports.TemplateError(err); } err.Update(path); // Unless they marked the dev flag, show them a trace from here if (!withInternals) { const old = err; err = new Error(old.message); err.name = old.name; } return err; } exports._prettifyError = _prettifyError; function TemplateError(message, lineno, colno) { var err; var cause; if (message instanceof Error) { cause = message; message = `${cause.name}: ${cause.message}`; } if (Object.setPrototypeOf) { err = new Error(message); Object.setPrototypeOf(err, TemplateError.prototype); } else { err = this; Object.defineProperty(err, 'message', { enumerable: false, writable: true, value: message, }); } Object.defineProperty(err, 'name', { value: 'Template render error', }); if (Error.captureStackTrace) { Error.captureStackTrace(err, this.constructor); } let getStack; if (cause) { const stackDescriptor = Object.getOwnPropertyDescriptor(cause, 'stack'); getStack = stackDescriptor && (stackDescriptor.get || (() => stackDescriptor.value)); if (!getStack) { getStack = () => cause.stack; } } else { const stack = (new Error(message)).stack; getStack = (() => stack); } Object.defineProperty(err, 'stack', { get: () => getStack.call(err), }); Object.defineProperty(err, 'cause', { value: cause }); err.lineno = lineno; err.colno = colno; err.firstUpdate = true; err.Update = function Update(path) { let msg = '(' + (path || 'unknown path') + ')'; // only show lineno + colno next to path of template // where error occurred if (this.firstUpdate) { if (this.lineno && this.colno) { msg += ` [Line ${this.lineno}, Column ${this.colno}]`; } else if (this.lineno) { msg += ` [Line ${this.lineno}]`; } } msg += '\n '; if (this.firstUpdate) { msg += ' '; } this.message = msg + (this.message || ''); this.firstUpdate = false; return this; }; return err; } if (Object.setPrototypeOf) { Object.setPrototypeOf(TemplateError.prototype, Error.prototype); } else { TemplateError.prototype = Object.create(Error.prototype, { constructor: { value: TemplateError, }, }); } exports.TemplateError = TemplateError; function escape(val) { return val.replace(escapeRegex, lookupEscape); } exports.escape = escape; function isFunction(obj) { return ObjProto.toString.call(obj) === '[object Function]'; } exports.isFunction = isFunction; function isArray(obj) { return ObjProto.toString.call(obj) === '[object Array]'; } exports.isArray = isArray; function isString(obj) { return ObjProto.toString.call(obj) === '[object String]'; } exports.isString = isString; function isObject(obj) { return ObjProto.toString.call(obj) === '[object Object]'; } exports.isObject = isObject; /** * @param {string|number} attr * @returns {(string|number)[]} * @private */ function _prepareAttributeParts(attr) { if (!attr) { return []; } if (typeof attr === 'string') { return attr.split('.'); } return [attr]; } /** * @param {string} attribute Attribute value. Dots allowed. * @returns {function(Object): *} */ function getAttrGetter(attribute) { const parts = _prepareAttributeParts(attribute); return function attrGetter(item) { let _item = item; for (let i = 0; i < parts.length; i++) { const part = parts[i]; // If item is not an object, and we still got parts to handle, it means // that something goes wrong. Just roll out to undefined in that case. if (hasOwnProp(_item, part)) { _item = _item[part]; } else { return undefined; } } return _item; }; } exports.getAttrGetter = getAttrGetter; function groupBy(obj, val, throwOnUndefined) { const result = {}; const iterator = isFunction(val) ? val : getAttrGetter(val); for (let i = 0; i < obj.length; i++) { const value = obj[i]; const key = iterator(value, i); if (key === undefined && throwOnUndefined === true) { throw new TypeError(`groupby: attribute "${val}" resolved to undefined`); } (result[key] || (result[key] = [])).push(value); } return result; } exports.groupBy = groupBy; function toArray(obj) { return Array.prototype.slice.call(obj); } exports.toArray = toArray; function without(array) { const result = []; if (!array) { return result; } const length = array.length; const contains = toArray(arguments).slice(1); let index = -1; while (++index < length) { if (indexOf(contains, array[index]) === -1) { result.push(array[index]); } } return result; } exports.without = without; function repeat(char_, n) { var str = ''; for (let i = 0; i < n; i++) { str += char_; } return str; } exports.repeat = repeat; function each(obj, func, context) { if (obj == null) { return; } if (ArrayProto.forEach && obj.forEach === ArrayProto.forEach) { obj.forEach(func, context); } else if (obj.length === +obj.length) { for (let i = 0, l = obj.length; i < l; i++) { func.call(context, obj[i], i, obj); } } } exports.each = each; function map(obj, func) { var results = []; if (obj == null) { return results; } if (ArrayProto.map && obj.map === ArrayProto.map) { return obj.map(func); } for (let i = 0; i < obj.length; i++) { results[results.length] = func(obj[i], i); } if (obj.length === +obj.length) { results.length = obj.length; } return results; } exports.map = map; function asyncIter(arr, iter, cb) { let i = -1; function next() { i++; if (i < arr.length) { iter(arr[i], i, next, cb); } else { cb(); } } next(); } exports.asyncIter = asyncIter; function asyncFor(obj, iter, cb) { const keys = keys_(obj || {}); const len = keys.length; let i = -1; function next() { i++; const k = keys[i]; if (i < len) { iter(k, obj[k], i, len, next); } else { cb(); } } next(); } exports.asyncFor = asyncFor; function indexOf(arr, searchElement, fromIndex) { return Array.prototype.indexOf.call(arr || [], searchElement, fromIndex); } exports.indexOf = indexOf; function keys_(obj) { /* eslint-disable no-restricted-syntax */ const arr = []; for (let k in obj) { if (hasOwnProp(obj, k)) { arr.push(k); } } return arr; } exports.keys = keys_; function _entries(obj) { return keys_(obj).map((k) => [k, obj[k]]); } exports._entries = _entries; function _values(obj) { return keys_(obj).map((k) => obj[k]); } exports._values = _values; function extend(obj1, obj2) { obj1 = obj1 || {}; keys_(obj2).forEach(k => { obj1[k] = obj2[k]; }); return obj1; } exports._assign = exports.extend = extend; function inOperator(key, val) { if (isArray(val) || isString(val)) { return val.indexOf(key) !== -1; } else if (isObject(val)) { return key in val; } throw new Error('Cannot use "in" operator to search for "' + key + '" in unexpected types.'); } exports.inOperator = inOperator; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/loader.js000664 000000 000000 00000000503 14012546311 024304 0ustar00rootroot000000 000000 'use strict'; const path = require('path'); const {EmitterObj} = require('./object'); module.exports = class Loader extends EmitterObj { resolve(from, to) { return path.resolve(path.dirname(from), to); } isRelative(filename) { return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); } }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/loaders.js000664 000000 000000 00000000211 14012546311 024463 0ustar00rootroot000000 000000 // This file will automatically be rewired to web-loader.js when // building for the browser module.exports = require('./node-loaders'); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/node-loaders.js000664 000000 000000 00000007351 14012546311 025422 0ustar00rootroot000000 000000 /* eslint-disable no-console */ 'use strict'; const fs = require('fs'); const path = require('path'); const Loader = require('./loader'); const {PrecompiledLoader} = require('./precompiled-loader.js'); let chokidar; class FileSystemLoader extends Loader { constructor(searchPaths, opts) { super(); if (typeof opts === 'boolean') { console.log( '[nunjucks] Warning: you passed a boolean as the second ' + 'argument to FileSystemLoader, but it now takes an options ' + 'object. See http://mozilla.github.io/nunjucks/api.html#filesystemloader' ); } opts = opts || {}; this.pathsToNames = {}; this.noCache = !!opts.noCache; if (searchPaths) { searchPaths = Array.isArray(searchPaths) ? searchPaths : [searchPaths]; // For windows, convert to forward slashes this.searchPaths = searchPaths.map(path.normalize); } else { this.searchPaths = ['.']; } if (opts.watch) { // Watch all the templates in the paths and fire an event when // they change try { chokidar = require('chokidar'); // eslint-disable-line global-require } catch (e) { throw new Error('watch requires chokidar to be installed'); } const paths = this.searchPaths.filter(fs.existsSync); const watcher = chokidar.watch(paths); watcher.on('all', (event, fullname) => { fullname = path.resolve(fullname); if (event === 'change' && fullname in this.pathsToNames) { this.emit('update', this.pathsToNames[fullname], fullname); } }); watcher.on('error', (error) => { console.log('Watcher error: ' + error); }); } } getSource(name) { var fullpath = null; var paths = this.searchPaths; for (let i = 0; i < paths.length; i++) { const basePath = path.resolve(paths[i]); const p = path.resolve(paths[i], name); // Only allow the current directory and anything // underneath it to be searched if (p.indexOf(basePath) === 0 && fs.existsSync(p)) { fullpath = p; break; } } if (!fullpath) { return null; } this.pathsToNames[fullpath] = name; const source = { src: fs.readFileSync(fullpath, 'utf-8'), path: fullpath, noCache: this.noCache }; this.emit('load', name, source); return source; } } class NodeResolveLoader extends Loader { constructor(opts) { super(); opts = opts || {}; this.pathsToNames = {}; this.noCache = !!opts.noCache; if (opts.watch) { try { chokidar = require('chokidar'); // eslint-disable-line global-require } catch (e) { throw new Error('watch requires chokidar to be installed'); } this.watcher = chokidar.watch(); this.watcher.on('change', (fullname) => { this.emit('update', this.pathsToNames[fullname], fullname); }); this.watcher.on('error', (error) => { console.log('Watcher error: ' + error); }); this.on('load', (name, source) => { this.watcher.add(source.path); }); } } getSource(name) { // Don't allow file-system traversal if ((/^\.?\.?(\/|\\)/).test(name)) { return null; } if ((/^[A-Z]:/).test(name)) { return null; } let fullpath; try { fullpath = require.resolve(name); } catch (e) { return null; } this.pathsToNames[fullpath] = name; const source = { src: fs.readFileSync(fullpath, 'utf-8'), path: fullpath, noCache: this.noCache, }; this.emit('load', name, source); return source; } } module.exports = { FileSystemLoader: FileSystemLoader, PrecompiledLoader: PrecompiledLoader, NodeResolveLoader: NodeResolveLoader, }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/nodes.js000664 000000 000000 00000016305 14012546311 024155 0ustar00rootroot000000 000000 'use strict'; const {Obj} = require('./object'); function traverseAndCheck(obj, type, results) { if (obj instanceof type) { results.push(obj); } if (obj instanceof Node) { obj.findAll(type, results); } } class Node extends Obj { init(lineno, colno, ...args) { this.lineno = lineno; this.colno = colno; this.fields.forEach((field, i) => { // The first two args are line/col numbers, so offset by 2 var val = arguments[i + 2]; // Fields should never be undefined, but null. It makes // testing easier to normalize values. if (val === undefined) { val = null; } this[field] = val; }); } findAll(type, results) { results = results || []; if (this instanceof NodeList) { this.children.forEach(child => traverseAndCheck(child, type, results)); } else { this.fields.forEach(field => traverseAndCheck(this[field], type, results)); } return results; } iterFields(func) { this.fields.forEach((field) => { func(this[field], field); }); } } // Abstract nodes class Value extends Node { get typename() { return 'Value'; } get fields() { return ['value']; } } // Concrete nodes class NodeList extends Node { get typename() { return 'NodeList'; } get fields() { return ['children']; } init(lineno, colno, nodes) { super.init(lineno, colno, nodes || []); } addChild(node) { this.children.push(node); } } const Root = NodeList.extend('Root'); const Literal = Value.extend('Literal'); const Symbol = Value.extend('Symbol'); const Group = NodeList.extend('Group'); const ArrayNode = NodeList.extend('Array'); const Pair = Node.extend('Pair', { fields: ['key', 'value'] }); const Dict = NodeList.extend('Dict'); const LookupVal = Node.extend('LookupVal', { fields: ['target', 'val'] }); const If = Node.extend('If', { fields: ['cond', 'body', 'else_'] }); const IfAsync = If.extend('IfAsync'); const InlineIf = Node.extend('InlineIf', { fields: ['cond', 'body', 'else_'] }); const For = Node.extend('For', { fields: ['arr', 'name', 'body', 'else_'] }); const AsyncEach = For.extend('AsyncEach'); const AsyncAll = For.extend('AsyncAll'); const Macro = Node.extend('Macro', { fields: ['name', 'args', 'body'] }); const Caller = Macro.extend('Caller'); const Import = Node.extend('Import', { fields: ['template', 'target', 'withContext'] }); class FromImport extends Node { get typename() { return 'FromImport'; } get fields() { return ['template', 'names', 'withContext']; } init(lineno, colno, template, names, withContext) { super.init(lineno, colno, template, names || new NodeList(), withContext); } } const FunCall = Node.extend('FunCall', { fields: ['name', 'args'] }); const Filter = FunCall.extend('Filter'); const FilterAsync = Filter.extend('FilterAsync', { fields: ['name', 'args', 'symbol'] }); const KeywordArgs = Dict.extend('KeywordArgs'); const Block = Node.extend('Block', { fields: ['name', 'body'] }); const Super = Node.extend('Super', { fields: ['blockName', 'symbol'] }); const TemplateRef = Node.extend('TemplateRef', { fields: ['template'] }); const Extends = TemplateRef.extend('Extends'); const Include = Node.extend('Include', { fields: ['template', 'ignoreMissing'] }); const Set = Node.extend('Set', { fields: ['targets', 'value'] }); const Switch = Node.extend('Switch', { fields: ['expr', 'cases', 'default'] }); const Case = Node.extend('Case', { fields: ['cond', 'body'] }); const Output = NodeList.extend('Output'); const Capture = Node.extend('Capture', { fields: ['body'] }); const TemplateData = Literal.extend('TemplateData'); const UnaryOp = Node.extend('UnaryOp', { fields: ['target'] }); const BinOp = Node.extend('BinOp', { fields: ['left', 'right'] }); const In = BinOp.extend('In'); const Is = BinOp.extend('Is'); const Or = BinOp.extend('Or'); const And = BinOp.extend('And'); const Not = UnaryOp.extend('Not'); const Add = BinOp.extend('Add'); const Concat = BinOp.extend('Concat'); const Sub = BinOp.extend('Sub'); const Mul = BinOp.extend('Mul'); const Div = BinOp.extend('Div'); const FloorDiv = BinOp.extend('FloorDiv'); const Mod = BinOp.extend('Mod'); const Pow = BinOp.extend('Pow'); const Neg = UnaryOp.extend('Neg'); const Pos = UnaryOp.extend('Pos'); const Compare = Node.extend('Compare', { fields: ['expr', 'ops'] }); const CompareOperand = Node.extend('CompareOperand', { fields: ['expr', 'type'] }); const CallExtension = Node.extend('CallExtension', { init(ext, prop, args, contentArgs) { this.parent(); this.extName = ext.__name || ext; this.prop = prop; this.args = args || new NodeList(); this.contentArgs = contentArgs || []; this.autoescape = ext.autoescape; }, fields: ['extName', 'prop', 'args', 'contentArgs'] }); const CallExtensionAsync = CallExtension.extend('CallExtensionAsync'); // This is hacky, but this is just a debugging function anyway function print(str, indent, inline) { var lines = str.split('\n'); lines.forEach((line, i) => { if (line && ((inline && i > 0) || !inline)) { process.stdout.write((' ').repeat(indent)); } const nl = (i === lines.length - 1) ? '' : '\n'; process.stdout.write(`${line}${nl}`); }); } // Print the AST in a nicely formatted tree format for debuggin function printNodes(node, indent) { indent = indent || 0; print(node.typename + ': ', indent); if (node instanceof NodeList) { print('\n'); node.children.forEach((n) => { printNodes(n, indent + 2); }); } else if (node instanceof CallExtension) { print(`${node.extName}.${node.prop}\n`); if (node.args) { printNodes(node.args, indent + 2); } if (node.contentArgs) { node.contentArgs.forEach((n) => { printNodes(n, indent + 2); }); } } else { let nodes = []; let props = null; node.iterFields((val, fieldName) => { if (val instanceof Node) { nodes.push([fieldName, val]); } else { props = props || {}; props[fieldName] = val; } }); if (props) { print(JSON.stringify(props, null, 2) + '\n', null, true); } else { print('\n'); } nodes.forEach(([fieldName, n]) => { print(`[${fieldName}] =>`, indent + 2); printNodes(n, indent + 4); }); } } module.exports = { Node: Node, Root: Root, NodeList: NodeList, Value: Value, Literal: Literal, Symbol: Symbol, Group: Group, Array: ArrayNode, Pair: Pair, Dict: Dict, Output: Output, Capture: Capture, TemplateData: TemplateData, If: If, IfAsync: IfAsync, InlineIf: InlineIf, For: For, AsyncEach: AsyncEach, AsyncAll: AsyncAll, Macro: Macro, Caller: Caller, Import: Import, FromImport: FromImport, FunCall: FunCall, Filter: Filter, FilterAsync: FilterAsync, KeywordArgs: KeywordArgs, Block: Block, Super: Super, Extends: Extends, Include: Include, Set: Set, Switch: Switch, Case: Case, LookupVal: LookupVal, BinOp: BinOp, In: In, Is: Is, Or: Or, And: And, Not: Not, Add: Add, Concat: Concat, Sub: Sub, Mul: Mul, Div: Div, FloorDiv: FloorDiv, Mod: Mod, Pow: Pow, Neg: Neg, Pos: Pos, Compare: Compare, CompareOperand: CompareOperand, CallExtension: CallExtension, CallExtensionAsync: CallExtensionAsync, printNodes: printNodes }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/object.js000664 000000 000000 00000003166 14012546311 024314 0ustar00rootroot000000 000000 'use strict'; // A simple class system, more documentation to come const EventEmitter = require('events'); const lib = require('./lib'); function parentWrap(parent, prop) { if (typeof parent !== 'function' || typeof prop !== 'function') { return prop; } return function wrap() { // Save the current parent method const tmp = this.parent; // Set parent to the previous method, call, and restore this.parent = parent; const res = prop.apply(this, arguments); this.parent = tmp; return res; }; } function extendClass(cls, name, props) { props = props || {}; lib.keys(props).forEach(k => { props[k] = parentWrap(cls.prototype[k], props[k]); }); class subclass extends cls { get typename() { return name; } } lib._assign(subclass.prototype, props); return subclass; } class Obj { constructor(...args) { // Unfortunately necessary for backwards compatibility this.init(...args); } init() {} get typename() { return this.constructor.name; } static extend(name, props) { if (typeof name === 'object') { props = name; name = 'anonymous'; } return extendClass(this, name, props); } } class EmitterObj extends EventEmitter { constructor(...args) { super(); // Unfortunately necessary for backwards compatibility this.init(...args); } init() {} get typename() { return this.constructor.name; } static extend(name, props) { if (typeof name === 'object') { props = name; name = 'anonymous'; } return extendClass(this, name, props); } } module.exports = { Obj, EmitterObj }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/parser.js000664 000000 000000 00000104253 14012546311 024341 0ustar00rootroot000000 000000 'use strict'; var lexer = require('./lexer'); var nodes = require('./nodes'); var Obj = require('./object').Obj; var lib = require('./lib'); class Parser extends Obj { init(tokens) { this.tokens = tokens; this.peeked = null; this.breakOnBlocks = null; this.dropLeadingWhitespace = false; this.extensions = []; } nextToken(withWhitespace) { var tok; if (this.peeked) { if (!withWhitespace && this.peeked.type === lexer.TOKEN_WHITESPACE) { this.peeked = null; } else { tok = this.peeked; this.peeked = null; return tok; } } tok = this.tokens.nextToken(); if (!withWhitespace) { while (tok && tok.type === lexer.TOKEN_WHITESPACE) { tok = this.tokens.nextToken(); } } return tok; } peekToken() { this.peeked = this.peeked || this.nextToken(); return this.peeked; } pushToken(tok) { if (this.peeked) { throw new Error('pushToken: can only push one token on between reads'); } this.peeked = tok; } error(msg, lineno, colno) { if (lineno === undefined || colno === undefined) { const tok = this.peekToken() || {}; lineno = tok.lineno; colno = tok.colno; } if (lineno !== undefined) { lineno += 1; } if (colno !== undefined) { colno += 1; } return new lib.TemplateError(msg, lineno, colno); } fail(msg, lineno, colno) { throw this.error(msg, lineno, colno); } skip(type) { var tok = this.nextToken(); if (!tok || tok.type !== type) { this.pushToken(tok); return false; } return true; } expect(type) { var tok = this.nextToken(); if (tok.type !== type) { this.fail('expected ' + type + ', got ' + tok.type, tok.lineno, tok.colno); } return tok; } skipValue(type, val) { var tok = this.nextToken(); if (!tok || tok.type !== type || tok.value !== val) { this.pushToken(tok); return false; } return true; } skipSymbol(val) { return this.skipValue(lexer.TOKEN_SYMBOL, val); } advanceAfterBlockEnd(name) { var tok; if (!name) { tok = this.peekToken(); if (!tok) { this.fail('unexpected end of file'); } if (tok.type !== lexer.TOKEN_SYMBOL) { this.fail('advanceAfterBlockEnd: expected symbol token or ' + 'explicit name to be passed'); } name = this.nextToken().value; } tok = this.nextToken(); if (tok && tok.type === lexer.TOKEN_BLOCK_END) { if (tok.value.charAt(0) === '-') { this.dropLeadingWhitespace = true; } } else { this.fail('expected block end in ' + name + ' statement'); } return tok; } advanceAfterVariableEnd() { var tok = this.nextToken(); if (tok && tok.type === lexer.TOKEN_VARIABLE_END) { this.dropLeadingWhitespace = tok.value.charAt( tok.value.length - this.tokens.tags.VARIABLE_END.length - 1 ) === '-'; } else { this.pushToken(tok); this.fail('expected variable end'); } } parseFor() { var forTok = this.peekToken(); var node; var endBlock; if (this.skipSymbol('for')) { node = new nodes.For(forTok.lineno, forTok.colno); endBlock = 'endfor'; } else if (this.skipSymbol('asyncEach')) { node = new nodes.AsyncEach(forTok.lineno, forTok.colno); endBlock = 'endeach'; } else if (this.skipSymbol('asyncAll')) { node = new nodes.AsyncAll(forTok.lineno, forTok.colno); endBlock = 'endall'; } else { this.fail('parseFor: expected for{Async}', forTok.lineno, forTok.colno); } node.name = this.parsePrimary(); if (!(node.name instanceof nodes.Symbol)) { this.fail('parseFor: variable name expected for loop'); } const type = this.peekToken().type; if (type === lexer.TOKEN_COMMA) { // key/value iteration const key = node.name; node.name = new nodes.Array(key.lineno, key.colno); node.name.addChild(key); while (this.skip(lexer.TOKEN_COMMA)) { const prim = this.parsePrimary(); node.name.addChild(prim); } } if (!this.skipSymbol('in')) { this.fail('parseFor: expected "in" keyword for loop', forTok.lineno, forTok.colno); } node.arr = this.parseExpression(); this.advanceAfterBlockEnd(forTok.value); node.body = this.parseUntilBlocks(endBlock, 'else'); if (this.skipSymbol('else')) { this.advanceAfterBlockEnd('else'); node.else_ = this.parseUntilBlocks(endBlock); } this.advanceAfterBlockEnd(); return node; } parseMacro() { const macroTok = this.peekToken(); if (!this.skipSymbol('macro')) { this.fail('expected macro'); } const name = this.parsePrimary(true); const args = this.parseSignature(); const node = new nodes.Macro(macroTok.lineno, macroTok.colno, name, args); this.advanceAfterBlockEnd(macroTok.value); node.body = this.parseUntilBlocks('endmacro'); this.advanceAfterBlockEnd(); return node; } parseCall() { // a call block is parsed as a normal FunCall, but with an added // 'caller' kwarg which is a Caller node. var callTok = this.peekToken(); if (!this.skipSymbol('call')) { this.fail('expected call'); } const callerArgs = this.parseSignature(true) || new nodes.NodeList(); const macroCall = this.parsePrimary(); this.advanceAfterBlockEnd(callTok.value); const body = this.parseUntilBlocks('endcall'); this.advanceAfterBlockEnd(); const callerName = new nodes.Symbol(callTok.lineno, callTok.colno, 'caller'); const callerNode = new nodes.Caller(callTok.lineno, callTok.colno, callerName, callerArgs, body); // add the additional caller kwarg, adding kwargs if necessary const args = macroCall.args.children; if (!(args[args.length - 1] instanceof nodes.KeywordArgs)) { args.push(new nodes.KeywordArgs()); } const kwargs = args[args.length - 1]; kwargs.addChild(new nodes.Pair(callTok.lineno, callTok.colno, callerName, callerNode)); return new nodes.Output(callTok.lineno, callTok.colno, [macroCall]); } parseWithContext() { var tok = this.peekToken(); var withContext = null; if (this.skipSymbol('with')) { withContext = true; } else if (this.skipSymbol('without')) { withContext = false; } if (withContext !== null) { if (!this.skipSymbol('context')) { this.fail('parseFrom: expected context after with/without', tok.lineno, tok.colno); } } return withContext; } parseImport() { var importTok = this.peekToken(); if (!this.skipSymbol('import')) { this.fail('parseImport: expected import', importTok.lineno, importTok.colno); } const template = this.parseExpression(); if (!this.skipSymbol('as')) { this.fail('parseImport: expected "as" keyword', importTok.lineno, importTok.colno); } const target = this.parseExpression(); const withContext = this.parseWithContext(); const node = new nodes.Import(importTok.lineno, importTok.colno, template, target, withContext); this.advanceAfterBlockEnd(importTok.value); return node; } parseFrom() { const fromTok = this.peekToken(); if (!this.skipSymbol('from')) { this.fail('parseFrom: expected from'); } const template = this.parseExpression(); if (!this.skipSymbol('import')) { this.fail('parseFrom: expected import', fromTok.lineno, fromTok.colno); } const names = new nodes.NodeList(); let withContext; while (1) { // eslint-disable-line no-constant-condition const nextTok = this.peekToken(); if (nextTok.type === lexer.TOKEN_BLOCK_END) { if (!names.children.length) { this.fail('parseFrom: Expected at least one import name', fromTok.lineno, fromTok.colno); } // Since we are manually advancing past the block end, // need to keep track of whitespace control (normally // this is done in `advanceAfterBlockEnd` if (nextTok.value.charAt(0) === '-') { this.dropLeadingWhitespace = true; } this.nextToken(); break; } if (names.children.length > 0 && !this.skip(lexer.TOKEN_COMMA)) { this.fail('parseFrom: expected comma', fromTok.lineno, fromTok.colno); } const name = this.parsePrimary(); if (name.value.charAt(0) === '_') { this.fail('parseFrom: names starting with an underscore cannot be imported', name.lineno, name.colno); } if (this.skipSymbol('as')) { const alias = this.parsePrimary(); names.addChild(new nodes.Pair(name.lineno, name.colno, name, alias)); } else { names.addChild(name); } withContext = this.parseWithContext(); } return new nodes.FromImport(fromTok.lineno, fromTok.colno, template, names, withContext); } parseBlock() { const tag = this.peekToken(); if (!this.skipSymbol('block')) { this.fail('parseBlock: expected block', tag.lineno, tag.colno); } const node = new nodes.Block(tag.lineno, tag.colno); node.name = this.parsePrimary(); if (!(node.name instanceof nodes.Symbol)) { this.fail('parseBlock: variable name expected', tag.lineno, tag.colno); } this.advanceAfterBlockEnd(tag.value); node.body = this.parseUntilBlocks('endblock'); this.skipSymbol('endblock'); this.skipSymbol(node.name.value); const tok = this.peekToken(); if (!tok) { this.fail('parseBlock: expected endblock, got end of file'); } this.advanceAfterBlockEnd(tok.value); return node; } parseExtends() { const tagName = 'extends'; const tag = this.peekToken(); if (!this.skipSymbol(tagName)) { this.fail('parseTemplateRef: expected ' + tagName); } const node = new nodes.Extends(tag.lineno, tag.colno); node.template = this.parseExpression(); this.advanceAfterBlockEnd(tag.value); return node; } parseInclude() { const tagName = 'include'; const tag = this.peekToken(); if (!this.skipSymbol(tagName)) { this.fail('parseInclude: expected ' + tagName); } const node = new nodes.Include(tag.lineno, tag.colno); node.template = this.parseExpression(); if (this.skipSymbol('ignore') && this.skipSymbol('missing')) { node.ignoreMissing = true; } this.advanceAfterBlockEnd(tag.value); return node; } parseIf() { const tag = this.peekToken(); let node; if (this.skipSymbol('if') || this.skipSymbol('elif') || this.skipSymbol('elseif')) { node = new nodes.If(tag.lineno, tag.colno); } else if (this.skipSymbol('ifAsync')) { node = new nodes.IfAsync(tag.lineno, tag.colno); } else { this.fail('parseIf: expected if, elif, or elseif', tag.lineno, tag.colno); } node.cond = this.parseExpression(); this.advanceAfterBlockEnd(tag.value); node.body = this.parseUntilBlocks('elif', 'elseif', 'else', 'endif'); const tok = this.peekToken(); switch (tok && tok.value) { case 'elseif': case 'elif': node.else_ = this.parseIf(); break; case 'else': this.advanceAfterBlockEnd(); node.else_ = this.parseUntilBlocks('endif'); this.advanceAfterBlockEnd(); break; case 'endif': node.else_ = null; this.advanceAfterBlockEnd(); break; default: this.fail('parseIf: expected elif, else, or endif, got end of file'); } return node; } parseSet() { const tag = this.peekToken(); if (!this.skipSymbol('set')) { this.fail('parseSet: expected set', tag.lineno, tag.colno); } const node = new nodes.Set(tag.lineno, tag.colno, []); let target; while ((target = this.parsePrimary())) { node.targets.push(target); if (!this.skip(lexer.TOKEN_COMMA)) { break; } } if (!this.skipValue(lexer.TOKEN_OPERATOR, '=')) { if (!this.skip(lexer.TOKEN_BLOCK_END)) { this.fail('parseSet: expected = or block end in set tag', tag.lineno, tag.colno); } else { node.body = new nodes.Capture( tag.lineno, tag.colno, this.parseUntilBlocks('endset') ); node.value = null; this.advanceAfterBlockEnd(); } } else { node.value = this.parseExpression(); this.advanceAfterBlockEnd(tag.value); } return node; } parseSwitch() { /* * Store the tag names in variables in case someone ever wants to * customize this. */ const switchStart = 'switch'; const switchEnd = 'endswitch'; const caseStart = 'case'; const caseDefault = 'default'; // Get the switch tag. const tag = this.peekToken(); // fail early if we get some unexpected tag. if ( !this.skipSymbol(switchStart) && !this.skipSymbol(caseStart) && !this.skipSymbol(caseDefault) ) { this.fail('parseSwitch: expected "switch," "case" or "default"', tag.lineno, tag.colno); } // parse the switch expression const expr = this.parseExpression(); // advance until a start of a case, a default case or an endswitch. this.advanceAfterBlockEnd(switchStart); this.parseUntilBlocks(caseStart, caseDefault, switchEnd); // this is the first case. it could also be an endswitch, we'll check. let tok = this.peekToken(); // create new variables for our cases and default case. const cases = []; let defaultCase; // while we're dealing with new cases nodes... do { // skip the start symbol and get the case expression this.skipSymbol(caseStart); const cond = this.parseExpression(); this.advanceAfterBlockEnd(switchStart); // get the body of the case node and add it to the array of cases. const body = this.parseUntilBlocks(caseStart, caseDefault, switchEnd); cases.push(new nodes.Case(tok.line, tok.col, cond, body)); // get our next case tok = this.peekToken(); } while (tok && tok.value === caseStart); // we either have a default case or a switch end. switch (tok.value) { case caseDefault: this.advanceAfterBlockEnd(); defaultCase = this.parseUntilBlocks(switchEnd); this.advanceAfterBlockEnd(); break; case switchEnd: this.advanceAfterBlockEnd(); break; default: // otherwise bail because EOF this.fail('parseSwitch: expected "case," "default" or "endswitch," got EOF.'); } // and return the switch node. return new nodes.Switch(tag.lineno, tag.colno, expr, cases, defaultCase); } parseStatement() { var tok = this.peekToken(); var node; if (tok.type !== lexer.TOKEN_SYMBOL) { this.fail('tag name expected', tok.lineno, tok.colno); } if (this.breakOnBlocks && lib.indexOf(this.breakOnBlocks, tok.value) !== -1) { return null; } switch (tok.value) { case 'raw': return this.parseRaw(); case 'verbatim': return this.parseRaw('verbatim'); case 'if': case 'ifAsync': return this.parseIf(); case 'for': case 'asyncEach': case 'asyncAll': return this.parseFor(); case 'block': return this.parseBlock(); case 'extends': return this.parseExtends(); case 'include': return this.parseInclude(); case 'set': return this.parseSet(); case 'macro': return this.parseMacro(); case 'call': return this.parseCall(); case 'import': return this.parseImport(); case 'from': return this.parseFrom(); case 'filter': return this.parseFilterStatement(); case 'switch': return this.parseSwitch(); default: if (this.extensions.length) { for (let i = 0; i < this.extensions.length; i++) { const ext = this.extensions[i]; if (lib.indexOf(ext.tags || [], tok.value) !== -1) { return ext.parse(this, nodes, lexer); } } } this.fail('unknown block tag: ' + tok.value, tok.lineno, tok.colno); } return node; } parseRaw(tagName) { tagName = tagName || 'raw'; const endTagName = 'end' + tagName; // Look for upcoming raw blocks (ignore all other kinds of blocks) const rawBlockRegex = new RegExp('([\\s\\S]*?){%\\s*(' + tagName + '|' + endTagName + ')\\s*(?=%})%}'); let rawLevel = 1; let str = ''; let matches = null; // Skip opening raw token // Keep this token to track line and column numbers const begun = this.advanceAfterBlockEnd(); // Exit when there's nothing to match // or when we've found the matching "endraw" block while ((matches = this.tokens._extractRegex(rawBlockRegex)) && rawLevel > 0) { const all = matches[0]; const pre = matches[1]; const blockName = matches[2]; // Adjust rawlevel if (blockName === tagName) { rawLevel += 1; } else if (blockName === endTagName) { rawLevel -= 1; } // Add to str if (rawLevel === 0) { // We want to exclude the last "endraw" str += pre; // Move tokenizer to beginning of endraw block this.tokens.backN(all.length - pre.length); } else { str += all; } } return new nodes.Output( begun.lineno, begun.colno, [new nodes.TemplateData(begun.lineno, begun.colno, str)] ); } parsePostfix(node) { let lookup; let tok = this.peekToken(); while (tok) { if (tok.type === lexer.TOKEN_LEFT_PAREN) { // Function call node = new nodes.FunCall(tok.lineno, tok.colno, node, this.parseSignature()); } else if (tok.type === lexer.TOKEN_LEFT_BRACKET) { // Reference lookup = this.parseAggregate(); if (lookup.children.length > 1) { this.fail('invalid index'); } node = new nodes.LookupVal(tok.lineno, tok.colno, node, lookup.children[0]); } else if (tok.type === lexer.TOKEN_OPERATOR && tok.value === '.') { // Reference this.nextToken(); const val = this.nextToken(); if (val.type !== lexer.TOKEN_SYMBOL) { this.fail('expected name as lookup value, got ' + val.value, val.lineno, val.colno); } // Make a literal string because it's not a variable // reference lookup = new nodes.Literal(val.lineno, val.colno, val.value); node = new nodes.LookupVal(tok.lineno, tok.colno, node, lookup); } else { break; } tok = this.peekToken(); } return node; } parseExpression() { var node = this.parseInlineIf(); return node; } parseInlineIf() { let node = this.parseOr(); if (this.skipSymbol('if')) { const condNode = this.parseOr(); const bodyNode = node; node = new nodes.InlineIf(node.lineno, node.colno); node.body = bodyNode; node.cond = condNode; if (this.skipSymbol('else')) { node.else_ = this.parseOr(); } else { node.else_ = null; } } return node; } parseOr() { let node = this.parseAnd(); while (this.skipSymbol('or')) { const node2 = this.parseAnd(); node = new nodes.Or(node.lineno, node.colno, node, node2); } return node; } parseAnd() { let node = this.parseNot(); while (this.skipSymbol('and')) { const node2 = this.parseNot(); node = new nodes.And(node.lineno, node.colno, node, node2); } return node; } parseNot() { const tok = this.peekToken(); if (this.skipSymbol('not')) { return new nodes.Not(tok.lineno, tok.colno, this.parseNot()); } return this.parseIn(); } parseIn() { let node = this.parseIs(); while (1) { // eslint-disable-line no-constant-condition // check if the next token is 'not' const tok = this.nextToken(); if (!tok) { break; } const invert = tok.type === lexer.TOKEN_SYMBOL && tok.value === 'not'; // if it wasn't 'not', put it back if (!invert) { this.pushToken(tok); } if (this.skipSymbol('in')) { const node2 = this.parseIs(); node = new nodes.In(node.lineno, node.colno, node, node2); if (invert) { node = new nodes.Not(node.lineno, node.colno, node); } } else { // if we'd found a 'not' but this wasn't an 'in', put back the 'not' if (invert) { this.pushToken(tok); } break; } } return node; } // I put this right after "in" in the operator precedence stack. That can // obviously be changed to be closer to Jinja. parseIs() { let node = this.parseCompare(); // look for an is if (this.skipSymbol('is')) { // look for a not const not = this.skipSymbol('not'); // get the next node const node2 = this.parseCompare(); // create an Is node using the next node and the info from our Is node. node = new nodes.Is(node.lineno, node.colno, node, node2); // if we have a Not, create a Not node from our Is node. if (not) { node = new nodes.Not(node.lineno, node.colno, node); } } // return the node. return node; } parseCompare() { const compareOps = ['==', '===', '!=', '!==', '<', '>', '<=', '>=']; const expr = this.parseConcat(); const ops = []; while (1) { // eslint-disable-line no-constant-condition const tok = this.nextToken(); if (!tok) { break; } else if (compareOps.indexOf(tok.value) !== -1) { ops.push(new nodes.CompareOperand(tok.lineno, tok.colno, this.parseConcat(), tok.value)); } else { this.pushToken(tok); break; } } if (ops.length) { return new nodes.Compare(ops[0].lineno, ops[0].colno, expr, ops); } else { return expr; } } // finds the '~' for string concatenation parseConcat() { let node = this.parseAdd(); while (this.skipValue(lexer.TOKEN_TILDE, '~')) { const node2 = this.parseAdd(); node = new nodes.Concat(node.lineno, node.colno, node, node2); } return node; } parseAdd() { let node = this.parseSub(); while (this.skipValue(lexer.TOKEN_OPERATOR, '+')) { const node2 = this.parseSub(); node = new nodes.Add(node.lineno, node.colno, node, node2); } return node; } parseSub() { let node = this.parseMul(); while (this.skipValue(lexer.TOKEN_OPERATOR, '-')) { const node2 = this.parseMul(); node = new nodes.Sub(node.lineno, node.colno, node, node2); } return node; } parseMul() { let node = this.parseDiv(); while (this.skipValue(lexer.TOKEN_OPERATOR, '*')) { const node2 = this.parseDiv(); node = new nodes.Mul(node.lineno, node.colno, node, node2); } return node; } parseDiv() { let node = this.parseFloorDiv(); while (this.skipValue(lexer.TOKEN_OPERATOR, '/')) { const node2 = this.parseFloorDiv(); node = new nodes.Div(node.lineno, node.colno, node, node2); } return node; } parseFloorDiv() { let node = this.parseMod(); while (this.skipValue(lexer.TOKEN_OPERATOR, '//')) { const node2 = this.parseMod(); node = new nodes.FloorDiv(node.lineno, node.colno, node, node2); } return node; } parseMod() { let node = this.parsePow(); while (this.skipValue(lexer.TOKEN_OPERATOR, '%')) { const node2 = this.parsePow(); node = new nodes.Mod(node.lineno, node.colno, node, node2); } return node; } parsePow() { let node = this.parseUnary(); while (this.skipValue(lexer.TOKEN_OPERATOR, '**')) { const node2 = this.parseUnary(); node = new nodes.Pow(node.lineno, node.colno, node, node2); } return node; } parseUnary(noFilters) { const tok = this.peekToken(); let node; if (this.skipValue(lexer.TOKEN_OPERATOR, '-')) { node = new nodes.Neg(tok.lineno, tok.colno, this.parseUnary(true)); } else if (this.skipValue(lexer.TOKEN_OPERATOR, '+')) { node = new nodes.Pos(tok.lineno, tok.colno, this.parseUnary(true)); } else { node = this.parsePrimary(); } if (!noFilters) { node = this.parseFilter(node); } return node; } parsePrimary(noPostfix) { const tok = this.nextToken(); let val; let node = null; if (!tok) { this.fail('expected expression, got end of file'); } else if (tok.type === lexer.TOKEN_STRING) { val = tok.value; } else if (tok.type === lexer.TOKEN_INT) { val = parseInt(tok.value, 10); } else if (tok.type === lexer.TOKEN_FLOAT) { val = parseFloat(tok.value); } else if (tok.type === lexer.TOKEN_BOOLEAN) { if (tok.value === 'true') { val = true; } else if (tok.value === 'false') { val = false; } else { this.fail('invalid boolean: ' + tok.value, tok.lineno, tok.colno); } } else if (tok.type === lexer.TOKEN_NONE) { val = null; } else if (tok.type === lexer.TOKEN_REGEX) { val = new RegExp(tok.value.body, tok.value.flags); } if (val !== undefined) { node = new nodes.Literal(tok.lineno, tok.colno, val); } else if (tok.type === lexer.TOKEN_SYMBOL) { node = new nodes.Symbol(tok.lineno, tok.colno, tok.value); } else { // See if it's an aggregate type, we need to push the // current delimiter token back on this.pushToken(tok); node = this.parseAggregate(); } if (!noPostfix) { node = this.parsePostfix(node); } if (node) { return node; } else { throw this.error(`unexpected token: ${tok.value}`, tok.lineno, tok.colno); } } parseFilterName() { const tok = this.expect(lexer.TOKEN_SYMBOL); let name = tok.value; while (this.skipValue(lexer.TOKEN_OPERATOR, '.')) { name += '.' + this.expect(lexer.TOKEN_SYMBOL).value; } return new nodes.Symbol(tok.lineno, tok.colno, name); } parseFilterArgs(node) { if (this.peekToken().type === lexer.TOKEN_LEFT_PAREN) { // Get a FunCall node and add the parameters to the // filter const call = this.parsePostfix(node); return call.args.children; } return []; } parseFilter(node) { while (this.skip(lexer.TOKEN_PIPE)) { const name = this.parseFilterName(); node = new nodes.Filter( name.lineno, name.colno, name, new nodes.NodeList( name.lineno, name.colno, [node].concat(this.parseFilterArgs(node)) ) ); } return node; } parseFilterStatement() { var filterTok = this.peekToken(); if (!this.skipSymbol('filter')) { this.fail('parseFilterStatement: expected filter'); } const name = this.parseFilterName(); const args = this.parseFilterArgs(name); this.advanceAfterBlockEnd(filterTok.value); const body = new nodes.Capture( name.lineno, name.colno, this.parseUntilBlocks('endfilter') ); this.advanceAfterBlockEnd(); const node = new nodes.Filter( name.lineno, name.colno, name, new nodes.NodeList( name.lineno, name.colno, [body].concat(args) ) ); return new nodes.Output( name.lineno, name.colno, [node] ); } parseAggregate() { var tok = this.nextToken(); var node; switch (tok.type) { case lexer.TOKEN_LEFT_PAREN: node = new nodes.Group(tok.lineno, tok.colno); break; case lexer.TOKEN_LEFT_BRACKET: node = new nodes.Array(tok.lineno, tok.colno); break; case lexer.TOKEN_LEFT_CURLY: node = new nodes.Dict(tok.lineno, tok.colno); break; default: return null; } while (1) { // eslint-disable-line no-constant-condition const type = this.peekToken().type; if (type === lexer.TOKEN_RIGHT_PAREN || type === lexer.TOKEN_RIGHT_BRACKET || type === lexer.TOKEN_RIGHT_CURLY) { this.nextToken(); break; } if (node.children.length > 0) { if (!this.skip(lexer.TOKEN_COMMA)) { this.fail('parseAggregate: expected comma after expression', tok.lineno, tok.colno); } } if (node instanceof nodes.Dict) { // TODO: check for errors const key = this.parsePrimary(); // We expect a key/value pair for dicts, separated by a // colon if (!this.skip(lexer.TOKEN_COLON)) { this.fail('parseAggregate: expected colon after dict key', tok.lineno, tok.colno); } // TODO: check for errors const value = this.parseExpression(); node.addChild(new nodes.Pair(key.lineno, key.colno, key, value)); } else { // TODO: check for errors const expr = this.parseExpression(); node.addChild(expr); } } return node; } parseSignature(tolerant, noParens) { let tok = this.peekToken(); if (!noParens && tok.type !== lexer.TOKEN_LEFT_PAREN) { if (tolerant) { return null; } else { this.fail('expected arguments', tok.lineno, tok.colno); } } if (tok.type === lexer.TOKEN_LEFT_PAREN) { tok = this.nextToken(); } const args = new nodes.NodeList(tok.lineno, tok.colno); const kwargs = new nodes.KeywordArgs(tok.lineno, tok.colno); let checkComma = false; while (1) { // eslint-disable-line no-constant-condition tok = this.peekToken(); if (!noParens && tok.type === lexer.TOKEN_RIGHT_PAREN) { this.nextToken(); break; } else if (noParens && tok.type === lexer.TOKEN_BLOCK_END) { break; } if (checkComma && !this.skip(lexer.TOKEN_COMMA)) { this.fail('parseSignature: expected comma after expression', tok.lineno, tok.colno); } else { const arg = this.parseExpression(); if (this.skipValue(lexer.TOKEN_OPERATOR, '=')) { kwargs.addChild( new nodes.Pair(arg.lineno, arg.colno, arg, this.parseExpression()) ); } else { args.addChild(arg); } } checkComma = true; } if (kwargs.children.length) { args.addChild(kwargs); } return args; } parseUntilBlocks(...blockNames) { const prev = this.breakOnBlocks; this.breakOnBlocks = blockNames; const ret = this.parse(); this.breakOnBlocks = prev; return ret; } parseNodes() { let tok; const buf = []; while ((tok = this.nextToken())) { if (tok.type === lexer.TOKEN_DATA) { let data = tok.value; const nextToken = this.peekToken(); const nextVal = nextToken && nextToken.value; // If the last token has "-" we need to trim the // leading whitespace of the data. This is marked with // the `dropLeadingWhitespace` variable. if (this.dropLeadingWhitespace) { // TODO: this could be optimized (don't use regex) data = data.replace(/^\s*/, ''); this.dropLeadingWhitespace = false; } // Same for the succeeding block start token if (nextToken && ((nextToken.type === lexer.TOKEN_BLOCK_START && nextVal.charAt(nextVal.length - 1) === '-') || (nextToken.type === lexer.TOKEN_VARIABLE_START && nextVal.charAt(this.tokens.tags.VARIABLE_START.length) === '-') || (nextToken.type === lexer.TOKEN_COMMENT && nextVal.charAt(this.tokens.tags.COMMENT_START.length) === '-'))) { // TODO: this could be optimized (don't use regex) data = data.replace(/\s*$/, ''); } buf.push(new nodes.Output(tok.lineno, tok.colno, [new nodes.TemplateData(tok.lineno, tok.colno, data)])); } else if (tok.type === lexer.TOKEN_BLOCK_START) { this.dropLeadingWhitespace = false; const n = this.parseStatement(); if (!n) { break; } buf.push(n); } else if (tok.type === lexer.TOKEN_VARIABLE_START) { const e = this.parseExpression(); this.dropLeadingWhitespace = false; this.advanceAfterVariableEnd(); buf.push(new nodes.Output(tok.lineno, tok.colno, [e])); } else if (tok.type === lexer.TOKEN_COMMENT) { this.dropLeadingWhitespace = tok.value.charAt( tok.value.length - this.tokens.tags.COMMENT_END.length - 1 ) === '-'; } else { // Ignore comments, otherwise this should be an error this.fail('Unexpected token at top-level: ' + tok.type, tok.lineno, tok.colno); } } return buf; } parse() { return new nodes.NodeList(0, 0, this.parseNodes()); } parseAsRoot() { return new nodes.Root(0, 0, this.parseNodes()); } } // var util = require('util'); // var l = lexer.lex('{%- if x -%}\n hello {% endif %}'); // var t; // while((t = l.nextToken())) { // console.log(util.inspect(t)); // } // var p = new Parser(lexer.lex('hello {% filter title %}' + // 'Hello madam how are you' + // '{% endfilter %}')); // var n = p.parseAsRoot(); // nodes.printNodes(n); module.exports = { parse(src, extensions, opts) { var p = new Parser(lexer.lex(src, opts)); if (extensions !== undefined) { p.extensions = extensions; } return p.parseAsRoot(); }, Parser: Parser }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompile-global.js000664 000000 000000 00000001153 14012546311 026435 0ustar00rootroot000000 000000 'use strict'; function precompileGlobal(templates, opts) { var out = ''; opts = opts || {}; for (let i = 0; i < templates.length; i++) { const name = JSON.stringify(templates[i].name); const template = templates[i].template; out += '(function() {' + '(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})' + '[' + name + '] = (function() {\n' + template + '\n})();\n'; if (opts.asFunction) { out += 'return function(ctx, cb) { return nunjucks.render(' + name + ', ctx, cb); }\n'; } out += '})();\n'; } return out; } module.exports = precompileGlobal; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompile.js000664 000000 000000 00000007211 14012546311 025200 0ustar00rootroot000000 000000 'use strict'; const fs = require('fs'); const path = require('path'); const {_prettifyError} = require('./lib'); const compiler = require('./compiler'); const {Environment} = require('./environment'); const precompileGlobal = require('./precompile-global'); function match(filename, patterns) { if (!Array.isArray(patterns)) { return false; } return patterns.some((pattern) => filename.match(pattern)); } function precompileString(str, opts) { opts = opts || {}; opts.isString = true; const env = opts.env || new Environment([]); const wrapper = opts.wrapper || precompileGlobal; if (!opts.name) { throw new Error('the "name" option is required when compiling a string'); } return wrapper([_precompile(str, opts.name, env)], opts); } function precompile(input, opts) { // The following options are available: // // * name: name of the template (auto-generated when compiling a directory) // * isString: input is a string, not a file path // * asFunction: generate a callable function // * force: keep compiling on error // * env: the Environment to use (gets extensions and async filters from it) // * include: which file/folders to include (folders are auto-included, files are auto-excluded) // * exclude: which file/folders to exclude (folders are auto-included, files are auto-excluded) // * wrapper: function(templates, opts) {...} // Customize the output format to store the compiled template. // By default, templates are stored in a global variable used by the runtime. // A custom loader will be necessary to load your custom wrapper. opts = opts || {}; const env = opts.env || new Environment([]); const wrapper = opts.wrapper || precompileGlobal; if (opts.isString) { return precompileString(input, opts); } const pathStats = fs.existsSync(input) && fs.statSync(input); const precompiled = []; const templates = []; function addTemplates(dir) { fs.readdirSync(dir).forEach((file) => { const filepath = path.join(dir, file); let subpath = filepath.substr(path.join(input, '/').length); const stat = fs.statSync(filepath); if (stat && stat.isDirectory()) { subpath += '/'; if (!match(subpath, opts.exclude)) { addTemplates(filepath); } } else if (match(subpath, opts.include)) { templates.push(filepath); } }); } if (pathStats.isFile()) { precompiled.push(_precompile( fs.readFileSync(input, 'utf-8'), opts.name || input, env )); } else if (pathStats.isDirectory()) { addTemplates(input); for (let i = 0; i < templates.length; i++) { const name = templates[i].replace(path.join(input, '/'), ''); try { precompiled.push(_precompile( fs.readFileSync(templates[i], 'utf-8'), name, env )); } catch (e) { if (opts.force) { // Don't stop generating the output if we're // forcing compilation. console.error(e); // eslint-disable-line no-console } else { throw e; } } } } return wrapper(precompiled, opts); } function _precompile(str, name, env) { env = env || new Environment([]); const asyncFilters = env.asyncFilters; const extensions = env.extensionsList; let template; name = name.replace(/\\/g, '/'); try { template = compiler.compile(str, asyncFilters, extensions, name, env.opts); } catch (err) { throw _prettifyError(name, false, err); } return { name: name, template: template }; } module.exports = { precompile: precompile, precompileString: precompileString }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompiled-loader.js000664 000000 000000 00000000727 14012546311 026615 0ustar00rootroot000000 000000 'use strict'; const Loader = require('./loader'); class PrecompiledLoader extends Loader { constructor(compiledTemplates) { super(); this.precompiled = compiledTemplates || {}; } getSource(name) { if (this.precompiled[name]) { return { src: { type: 'code', obj: this.precompiled[name] }, path: name }; } return null; } } module.exports = { PrecompiledLoader: PrecompiledLoader, }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/runtime.js000664 000000 000000 00000020432 14012546311 024524 0ustar00rootroot000000 000000 'use strict'; var lib = require('./lib'); var arrayFrom = Array.from; var supportsIterators = ( typeof Symbol === 'function' && Symbol.iterator && typeof arrayFrom === 'function' ); // Frames keep track of scoping both at compile-time and run-time so // we know how to access variables. Block tags can introduce special // variables, for example. class Frame { constructor(parent, isolateWrites) { this.variables = Object.create(null); this.parent = parent; this.topLevel = false; // if this is true, writes (set) should never propagate upwards past // this frame to its parent (though reads may). this.isolateWrites = isolateWrites; } set(name, val, resolveUp) { // Allow variables with dots by automatically creating the // nested structure var parts = name.split('.'); var obj = this.variables; var frame = this; if (resolveUp) { if ((frame = this.resolve(parts[0], true))) { frame.set(name, val); return; } } for (let i = 0; i < parts.length - 1; i++) { const id = parts[i]; if (!obj[id]) { obj[id] = {}; } obj = obj[id]; } obj[parts[parts.length - 1]] = val; } get(name) { var val = this.variables[name]; if (val !== undefined) { return val; } return null; } lookup(name) { var p = this.parent; var val = this.variables[name]; if (val !== undefined) { return val; } return p && p.lookup(name); } resolve(name, forWrite) { var p = (forWrite && this.isolateWrites) ? undefined : this.parent; var val = this.variables[name]; if (val !== undefined) { return this; } return p && p.resolve(name); } push(isolateWrites) { return new Frame(this, isolateWrites); } pop() { return this.parent; } } function makeMacro(argNames, kwargNames, func) { return function macro(...macroArgs) { var argCount = numArgs(macroArgs); var args; var kwargs = getKeywordArgs(macroArgs); if (argCount > argNames.length) { args = macroArgs.slice(0, argNames.length); // Positional arguments that should be passed in as // keyword arguments (essentially default values) macroArgs.slice(args.length, argCount).forEach((val, i) => { if (i < kwargNames.length) { kwargs[kwargNames[i]] = val; } }); args.push(kwargs); } else if (argCount < argNames.length) { args = macroArgs.slice(0, argCount); for (let i = argCount; i < argNames.length; i++) { const arg = argNames[i]; // Keyword arguments that should be passed as // positional arguments, i.e. the caller explicitly // used the name of a positional arg args.push(kwargs[arg]); delete kwargs[arg]; } args.push(kwargs); } else { args = macroArgs; } return func.apply(this, args); }; } function makeKeywordArgs(obj) { obj.__keywords = true; return obj; } function isKeywordArgs(obj) { return obj && Object.prototype.hasOwnProperty.call(obj, '__keywords'); } function getKeywordArgs(args) { var len = args.length; if (len) { const lastArg = args[len - 1]; if (isKeywordArgs(lastArg)) { return lastArg; } } return {}; } function numArgs(args) { var len = args.length; if (len === 0) { return 0; } const lastArg = args[len - 1]; if (isKeywordArgs(lastArg)) { return len - 1; } else { return len; } } // A SafeString object indicates that the string should not be // autoescaped. This happens magically because autoescaping only // occurs on primitive string objects. function SafeString(val) { if (typeof val !== 'string') { return val; } this.val = val; this.length = val.length; } SafeString.prototype = Object.create(String.prototype, { length: { writable: true, configurable: true, value: 0 } }); SafeString.prototype.valueOf = function valueOf() { return this.val; }; SafeString.prototype.toString = function toString() { return this.val; }; function copySafeness(dest, target) { if (dest instanceof SafeString) { return new SafeString(target); } return target.toString(); } function markSafe(val) { var type = typeof val; if (type === 'string') { return new SafeString(val); } else if (type !== 'function') { return val; } else { return function wrapSafe(args) { var ret = val.apply(this, arguments); if (typeof ret === 'string') { return new SafeString(ret); } return ret; }; } } function suppressValue(val, autoescape) { val = (val !== undefined && val !== null) ? val : ''; if (autoescape && !(val instanceof SafeString)) { val = lib.escape(val.toString()); } return val; } function ensureDefined(val, lineno, colno) { if (val === null || val === undefined) { throw new lib.TemplateError( 'attempted to output null or undefined value', lineno + 1, colno + 1 ); } return val; } function memberLookup(obj, val) { if (obj === undefined || obj === null) { return undefined; } if (typeof obj[val] === 'function') { return (...args) => obj[val].apply(obj, args); } return obj[val]; } function callWrap(obj, name, context, args) { if (!obj) { throw new Error('Unable to call `' + name + '`, which is undefined or falsey'); } else if (typeof obj !== 'function') { throw new Error('Unable to call `' + name + '`, which is not a function'); } return obj.apply(context, args); } function contextOrFrameLookup(context, frame, name) { var val = frame.lookup(name); return (val !== undefined) ? val : context.lookup(name); } function handleError(error, lineno, colno) { if (error.lineno) { return error; } else { return new lib.TemplateError(error, lineno, colno); } } function asyncEach(arr, dimen, iter, cb) { if (lib.isArray(arr)) { const len = arr.length; lib.asyncIter(arr, function iterCallback(item, i, next) { switch (dimen) { case 1: iter(item, i, len, next); break; case 2: iter(item[0], item[1], i, len, next); break; case 3: iter(item[0], item[1], item[2], i, len, next); break; default: item.push(i, len, next); iter.apply(this, item); } }, cb); } else { lib.asyncFor(arr, function iterCallback(key, val, i, len, next) { iter(key, val, i, len, next); }, cb); } } function asyncAll(arr, dimen, func, cb) { var finished = 0; var len; var outputArr; function done(i, output) { finished++; outputArr[i] = output; if (finished === len) { cb(null, outputArr.join('')); } } if (lib.isArray(arr)) { len = arr.length; outputArr = new Array(len); if (len === 0) { cb(null, ''); } else { for (let i = 0; i < arr.length; i++) { const item = arr[i]; switch (dimen) { case 1: func(item, i, len, done); break; case 2: func(item[0], item[1], i, len, done); break; case 3: func(item[0], item[1], item[2], i, len, done); break; default: item.push(i, len, done); func.apply(this, item); } } } } else { const keys = lib.keys(arr || {}); len = keys.length; outputArr = new Array(len); if (len === 0) { cb(null, ''); } else { for (let i = 0; i < keys.length; i++) { const k = keys[i]; func(k, arr[k], i, len, done); } } } } function fromIterator(arr) { if (typeof arr !== 'object' || arr === null || lib.isArray(arr)) { return arr; } else if (supportsIterators && Symbol.iterator in arr) { return arrayFrom(arr); } else { return arr; } } module.exports = { Frame: Frame, makeMacro: makeMacro, makeKeywordArgs: makeKeywordArgs, numArgs: numArgs, suppressValue: suppressValue, ensureDefined: ensureDefined, memberLookup: memberLookup, contextOrFrameLookup: contextOrFrameLookup, callWrap: callWrap, handleError: handleError, isArray: lib.isArray, keys: lib.keys, SafeString: SafeString, copySafeness: copySafeness, markSafe: markSafe, asyncEach: asyncEach, asyncAll: asyncAll, inOperator: lib.inOperator, fromIterator: fromIterator }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/tests.js000664 000000 000000 00000013213 14012546311 024202 0ustar00rootroot000000 000000 'use strict'; var SafeString = require('./runtime').SafeString; /** * Returns `true` if the object is a function, otherwise `false`. * @param { any } value * @returns { boolean } */ function callable(value) { return typeof value === 'function'; } exports.callable = callable; /** * Returns `true` if the object is strictly not `undefined`. * @param { any } value * @returns { boolean } */ function defined(value) { return value !== undefined; } exports.defined = defined; /** * Returns `true` if the operand (one) is divisble by the test's argument * (two). * @param { number } one * @param { number } two * @returns { boolean } */ function divisibleby(one, two) { return (one % two) === 0; } exports.divisibleby = divisibleby; /** * Returns true if the string has been escaped (i.e., is a SafeString). * @param { any } value * @returns { boolean } */ function escaped(value) { return value instanceof SafeString; } exports.escaped = escaped; /** * Returns `true` if the arguments are strictly equal. * @param { any } one * @param { any } two */ function equalto(one, two) { return one === two; } exports.equalto = equalto; // Aliases exports.eq = exports.equalto; exports.sameas = exports.equalto; /** * Returns `true` if the value is evenly divisible by 2. * @param { number } value * @returns { boolean } */ function even(value) { return value % 2 === 0; } exports.even = even; /** * Returns `true` if the value is falsy - if I recall correctly, '', 0, false, * undefined, NaN or null. I don't know if we should stick to the default JS * behavior or attempt to replicate what Python believes should be falsy (i.e., * empty arrays, empty dicts, not 0...). * @param { any } value * @returns { boolean } */ function falsy(value) { return !value; } exports.falsy = falsy; /** * Returns `true` if the operand (one) is greater or equal to the test's * argument (two). * @param { number } one * @param { number } two * @returns { boolean } */ function ge(one, two) { return one >= two; } exports.ge = ge; /** * Returns `true` if the operand (one) is greater than the test's argument * (two). * @param { number } one * @param { number } two * @returns { boolean } */ function greaterthan(one, two) { return one > two; } exports.greaterthan = greaterthan; // alias exports.gt = exports.greaterthan; /** * Returns `true` if the operand (one) is less than or equal to the test's * argument (two). * @param { number } one * @param { number } two * @returns { boolean } */ function le(one, two) { return one <= two; } exports.le = le; /** * Returns `true` if the operand (one) is less than the test's passed argument * (two). * @param { number } one * @param { number } two * @returns { boolean } */ function lessthan(one, two) { return one < two; } exports.lessthan = lessthan; // alias exports.lt = exports.lessthan; /** * Returns `true` if the string is lowercased. * @param { string } value * @returns { boolean } */ function lower(value) { return value.toLowerCase() === value; } exports.lower = lower; /** * Returns `true` if the operand (one) is less than or equal to the test's * argument (two). * @param { number } one * @param { number } two * @returns { boolean } */ function ne(one, two) { return one !== two; } exports.ne = ne; /** * Returns true if the value is strictly equal to `null`. * @param { any } * @returns { boolean } */ function nullTest(value) { return value === null; } exports.null = nullTest; /** * Returns true if value is a number. * @param { any } * @returns { boolean } */ function number(value) { return typeof value === 'number'; } exports.number = number; /** * Returns `true` if the value is *not* evenly divisible by 2. * @param { number } value * @returns { boolean } */ function odd(value) { return value % 2 === 1; } exports.odd = odd; /** * Returns `true` if the value is a string, `false` if not. * @param { any } value * @returns { boolean } */ function string(value) { return typeof value === 'string'; } exports.string = string; /** * Returns `true` if the value is not in the list of things considered falsy: * '', null, undefined, 0, NaN and false. * @param { any } value * @returns { boolean } */ function truthy(value) { return !!value; } exports.truthy = truthy; /** * Returns `true` if the value is undefined. * @param { any } value * @returns { boolean } */ function undefinedTest(value) { return value === undefined; } exports.undefined = undefinedTest; /** * Returns `true` if the string is uppercased. * @param { string } value * @returns { boolean } */ function upper(value) { return value.toUpperCase() === value; } exports.upper = upper; /** * If ES6 features are available, returns `true` if the value implements the * `Symbol.iterator` method. If not, it's a string or Array. * * Could potentially cause issues if a browser exists that has Set and Map but * not Symbol. * * @param { any } value * @returns { boolean } */ function iterable(value) { if (typeof Symbol !== 'undefined') { return !!value[Symbol.iterator]; } else { return Array.isArray(value) || typeof value === 'string'; } } exports.iterable = iterable; /** * If ES6 features are available, returns `true` if the value is an object hash * or an ES6 Map. Otherwise just return if it's an object hash. * @param { any } value * @returns { boolean } */ function mapping(value) { // only maps and object hashes var bool = value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value); if (Set) { return bool && !(value instanceof Set); } else { return bool; } } exports.mapping = mapping; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/transformer.js000664 000000 000000 00000012324 14012546311 025404 0ustar00rootroot000000 000000 'use strict'; var nodes = require('./nodes'); var lib = require('./lib'); var sym = 0; function gensym() { return 'hole_' + sym++; } // copy-on-write version of map function mapCOW(arr, func) { var res = null; for (let i = 0; i < arr.length; i++) { const item = func(arr[i]); if (item !== arr[i]) { if (!res) { res = arr.slice(); } res[i] = item; } } return res || arr; } function walk(ast, func, depthFirst) { if (!(ast instanceof nodes.Node)) { return ast; } if (!depthFirst) { const astT = func(ast); if (astT && astT !== ast) { return astT; } } if (ast instanceof nodes.NodeList) { const children = mapCOW(ast.children, (node) => walk(node, func, depthFirst)); if (children !== ast.children) { ast = new nodes[ast.typename](ast.lineno, ast.colno, children); } } else if (ast instanceof nodes.CallExtension) { const args = walk(ast.args, func, depthFirst); const contentArgs = mapCOW(ast.contentArgs, (node) => walk(node, func, depthFirst)); if (args !== ast.args || contentArgs !== ast.contentArgs) { ast = new nodes[ast.typename](ast.extName, ast.prop, args, contentArgs); } } else { const props = ast.fields.map((field) => ast[field]); const propsT = mapCOW(props, (prop) => walk(prop, func, depthFirst)); if (propsT !== props) { ast = new nodes[ast.typename](ast.lineno, ast.colno); propsT.forEach((prop, i) => { ast[ast.fields[i]] = prop; }); } } return depthFirst ? (func(ast) || ast) : ast; } function depthWalk(ast, func) { return walk(ast, func, true); } function _liftFilters(node, asyncFilters, prop) { var children = []; var walked = depthWalk(prop ? node[prop] : node, (descNode) => { let symbol; if (descNode instanceof nodes.Block) { return descNode; } else if ((descNode instanceof nodes.Filter && lib.indexOf(asyncFilters, descNode.name.value) !== -1) || descNode instanceof nodes.CallExtensionAsync) { symbol = new nodes.Symbol(descNode.lineno, descNode.colno, gensym()); children.push(new nodes.FilterAsync(descNode.lineno, descNode.colno, descNode.name, descNode.args, symbol)); } return symbol; }); if (prop) { node[prop] = walked; } else { node = walked; } if (children.length) { children.push(node); return new nodes.NodeList( node.lineno, node.colno, children ); } else { return node; } } function liftFilters(ast, asyncFilters) { return depthWalk(ast, (node) => { if (node instanceof nodes.Output) { return _liftFilters(node, asyncFilters); } else if (node instanceof nodes.Set) { return _liftFilters(node, asyncFilters, 'value'); } else if (node instanceof nodes.For) { return _liftFilters(node, asyncFilters, 'arr'); } else if (node instanceof nodes.If) { return _liftFilters(node, asyncFilters, 'cond'); } else if (node instanceof nodes.CallExtension) { return _liftFilters(node, asyncFilters, 'args'); } else { return undefined; } }); } function liftSuper(ast) { return walk(ast, (blockNode) => { if (!(blockNode instanceof nodes.Block)) { return; } let hasSuper = false; const symbol = gensym(); blockNode.body = walk(blockNode.body, (node) => { // eslint-disable-line consistent-return if (node instanceof nodes.FunCall && node.name.value === 'super') { hasSuper = true; return new nodes.Symbol(node.lineno, node.colno, symbol); } }); if (hasSuper) { blockNode.body.children.unshift(new nodes.Super( 0, 0, blockNode.name, new nodes.Symbol(0, 0, symbol) )); } }); } function convertStatements(ast) { return depthWalk(ast, (node) => { if (!(node instanceof nodes.If) && !(node instanceof nodes.For)) { return undefined; } let async = false; walk(node, (child) => { if (child instanceof nodes.FilterAsync || child instanceof nodes.IfAsync || child instanceof nodes.AsyncEach || child instanceof nodes.AsyncAll || child instanceof nodes.CallExtensionAsync) { async = true; // Stop iterating by returning the node return child; } return undefined; }); if (async) { if (node instanceof nodes.If) { return new nodes.IfAsync( node.lineno, node.colno, node.cond, node.body, node.else_ ); } else if (node instanceof nodes.For && !(node instanceof nodes.AsyncAll)) { return new nodes.AsyncEach( node.lineno, node.colno, node.arr, node.name, node.body, node.else_ ); } } return undefined; }); } function cps(ast, asyncFilters) { return convertStatements(liftSuper(liftFilters(ast, asyncFilters))); } function transform(ast, asyncFilters) { return cps(ast, asyncFilters || []); } // var parser = require('./parser'); // var src = 'hello {% foo %}{% endfoo %} end'; // var ast = transform(parser.parse(src, [new FooExtension()]), ['bar']); // nodes.printNodes(ast); module.exports = { transform: transform }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/web-loaders.js000664 000000 000000 00000004671 14012546311 025254 0ustar00rootroot000000 000000 'use strict'; const Loader = require('./loader'); const {PrecompiledLoader} = require('./precompiled-loader.js'); class WebLoader extends Loader { constructor(baseURL, opts) { super(); this.baseURL = baseURL || '.'; opts = opts || {}; // By default, the cache is turned off because there's no way // to "watch" templates over HTTP, so they are re-downloaded // and compiled each time. (Remember, PRECOMPILE YOUR // TEMPLATES in production!) this.useCache = !!opts.useCache; // We default `async` to false so that the simple synchronous // API can be used when you aren't doing anything async in // your templates (which is most of the time). This performs a // sync ajax request, but that's ok because it should *only* // happen in development. PRECOMPILE YOUR TEMPLATES. this.async = !!opts.async; } resolve(from, to) { throw new Error('relative templates not support in the browser yet'); } getSource(name, cb) { var useCache = this.useCache; var result; this.fetch(this.baseURL + '/' + name, (err, src) => { if (err) { if (cb) { cb(err.content); } else if (err.status === 404) { result = null; } else { throw err.content; } } else { result = { src: src, path: name, noCache: !useCache }; this.emit('load', name, result); if (cb) { cb(null, result); } } }); // if this WebLoader isn't running asynchronously, the // fetch above would actually run sync and we'll have a // result here return result; } fetch(url, cb) { // Only in the browser please if (typeof window === 'undefined') { throw new Error('WebLoader can only by used in a browser'); } const ajax = new XMLHttpRequest(); let loading = true; ajax.onreadystatechange = () => { if (ajax.readyState === 4 && loading) { loading = false; if (ajax.status === 0 || ajax.status === 200) { cb(null, ajax.responseText); } else { cb({ status: ajax.status, content: ajax.responseText }); } } }; url += (url.indexOf('?') === -1 ? '?' : '&') + 's=' + (new Date().getTime()); ajax.open('GET', url, this.async); ajax.send(); } } module.exports = { WebLoader: WebLoader, PrecompiledLoader: PrecompiledLoader }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/package.json000664 000000 000000 00000006102 14012546311 022340 0ustar00rootroot000000 000000 { "name": "nunjucks", "description": "A powerful templating engine with inheritance, asynchronous control, and more (jinja2 inspired)", "version": "3.2.3", "author": "James Long ", "dependencies": { "a-sync-waterfall": "^1.0.0", "asap": "^2.0.3", "commander": "^5.1.0" }, "browser": "./browser/nunjucks.js", "devDependencies": { "@babel/cli": "^7.0.0-beta.38", "@babel/core": "^7.0.0-beta.38", "@babel/preset-env": "^7.0.0-beta.38", "@babel/register": "^7.0.0-beta.38", "babel-loader": "^8.0.0-beta.0", "babel-plugin-istanbul": "^4.1.5", "babel-plugin-module-resolver": "3.0.0-beta.5", "connect": "^3.6.5", "core-js": "^2.5.3", "cross-env": "^5.1.3", "eslint": "^4.13.0", "eslint-config-airbnb-base": "^12.1.0", "eslint-plugin-import": "^2.8.0", "expect.js": "*", "express": "4.x", "fs-extra": "^5.0.0", "get-port": "^3.2.0", "mocha": "< 8.x", "mocha-phantomjs-core": "^2.1.2", "mocha-phantomjs-istanbul": "0.0.2", "module-alias": "^2.0.3", "node-libs-browser": "^0.7.0", "nyc": "^11.4.1", "phantomjs-prebuilt": "^2.1.16", "serve-static": "^1.13.1", "supertest": "*", "uglify-js": "^2.8.29", "uglifyjs-webpack-plugin": "^1.1.6", "webpack": "^3.10.0" }, "buildDependencies": { "@babel/cli": "^7.0.0-beta.38", "@babel/core": "^7.0.0-beta.38", "@babel/preset-env": "^7.0.0-beta.38", "@babel/register": "^7.0.0-beta.38", "babel-loader": "^8.0.0-beta.0", "babel-plugin-istanbul": "^4.1.5", "babel-plugin-module-resolver": "3.0.0-beta.5", "core-js": "^2.5.3", "module-alias": "^2.0.3", "node-libs-browser": "^0.7.0", "uglify-js": "^2.8.29", "uglifyjs-webpack-plugin": "^1.1.6", "webpack": "^3.10.0" }, "peerDependencies": { "chokidar": "^3.3.0" }, "peerDependenciesMeta": { "chokidar": { "optional": true } }, "_moduleAliases": { "babel-register": "@babel/register" }, "engines": { "node": ">= 6.9.0" }, "scripts": { "build:transpile": "babel nunjucks --out-dir .", "build:bundle": "node scripts/bundle.js", "build": "npm run build:transpile && npm run build:bundle", "codecov": "codecov", "mocha": "mocha -R spec tests", "lint": "eslint nunjucks scripts tests", "prepare": "npm run build", "test:instrument": "cross-env NODE_ENV=test scripts/bundle.js", "test:runner": "cross-env NODE_ENV=test NODE_PATH=tests/test-node-pkgs scripts/testrunner.js", "test": "npm run lint && npm run test:instrument && npm run test:runner" }, "bin": { "nunjucks-precompile": "./bin/precompile" }, "main": "index.js", "files": [ "bin/**", "browser/**", "src/**" ], "nyc": { "require": [ "babel-register" ], "sourceMap": false, "instrument": false }, "repository": { "type": "git", "url": "https://github.com/mozilla/nunjucks.git" }, "keywords": [ "template", "templating" ], "license": "BSD-2-Clause", "bugs": { "url": "https://github.com/mozilla/nunjucks/issues" } } nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/000775 000000 000000 00000000000 14012546311 021517 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/000775 000000 000000 00000000000 14012546311 023210 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/js/000775 000000 000000 00000000000 14012546311 023624 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/js/app.js000664 000000 000000 00000000220 14012546311 024734 0ustar00rootroot000000 000000 nunjucks.configure('views', { autoescape: true }); // aboutTmpl({ poop: 'pooop<><>' }, function(err, res) { // console.log(res); // }); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/js/extensions.js000664 000000 000000 00000002553 14012546311 026366 0ustar00rootroot000000 000000 function RemoteExtension() { this.tags = ['remote']; this.parse = function(parser, nodes, lexer) { // get the tag token var tok = parser.nextToken(); // parse the args and move after the block end. passing true // as the second arg is required if there are no parentheses var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); // parse the body and move after block end var body = parser.parseUntilBlocks('error', 'endtruncate'); var errorBody = null; if (parser.skipSymbol('error')) { parser.skip(lexer.TOKEN_BLOCK_END); errorBody = parser.parseUntilBlocks('endremote'); } parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, 'run', args, [body, errorBody]); }; this.run = function(context, url, body, errorBody) { var id = 'el' + Math.floor(Math.random() * 10000); var ret = new nunjucks.runtime.SafeString('
' + body() + '
'); var ajax = new XMLHttpRequest(); ajax.onreadystatechange = function() { if (ajax.readyState == 4) { if (ajax.status == 200) { document.getElementById(id).innerHTML = ajax.responseText; } else { document.getElementById(id).innerHTML = errorBody(); } } }; ajax.open('GET', url, true); ajax.send(); return ret; }; } nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/main.js000664 000000 000000 00000001270 14012546311 024472 0ustar00rootroot000000 000000 /* eslint-disable func-names */ 'use strict'; var path = require('path'); var nunjucks = require('../..'); var express = require('express'); var app = express(); nunjucks.configure(path.join(__dirname, 'views'), { autoescape: true, express: app, watch: true }); // app app.use(express.static(__dirname)); app.use(function(req, res, next) { res.locals.user = 'hello'; next(); }); app.get('/', function(req, res) { res.render('index.html', { username: 'James Long copyright' }); }); app.get('/about', function(req, res) { res.render('about.html'); }); app.listen(4000, function() { console.log('Express server running on http://localhost:4000'); }); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/pre.js000664 000000 000000 00000001314 14012546311 024333 0ustar00rootroot000000 000000 #!/usr/bin/env node 'use strict'; var precompileString = require('../..').precompileString; var fs = require('fs'); var path = require('path'); var out = 'window.baseTmpl = ' + precompileString( fs.readFileSync(path.join(__dirname, 'views/base.html'), 'utf-8'), { name: 'base.html', asFunction: true }); out += 'window.aboutTmpl = ' + precompileString( fs.readFileSync(path.join(__dirname, 'views/about.html'), 'utf-8'), { name: 'about.html', asFunction: true }); fs.writeFileSync(path.join(__dirname, 'js/templates.js'), out, 'utf-8'); fs.writeFileSync(path.join(__dirname, 'js/nunjucks.js'), fs.readFileSync(path.join(__dirname, '../../browser/nunjucks.js'), 'utf-8'), 'utf-8'); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/000775 000000 000000 00000000000 14012546311 024345 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/about.html000664 000000 000000 00000000264 14012546311 026347 0ustar00rootroot000000 000000 {% extends "base.html" %} {% block content %} This is just the about page {% endblock %} {% block footer %} {{ super() }} You really should read this! {{ poop }} {% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/base.html000664 000000 000000 00000001010 14012546311 026135 0ustar00rootroot000000 000000 A quick app {% block content %}{% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/import-context-set.html000664 000000 000000 00000000026 14012546311 031016 0ustar00rootroot000000 000000 {% set bar = "FOO" %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/index.html000664 000000 000000 00000000243 14012546311 026341 0ustar00rootroot000000 000000 {% extends "base.html" %} {% block content %} Hello, {{ username | default('poop') | safe }}! This is just some content.
{% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/item-base.html000664 000000 000000 00000000145 14012546311 027101 0ustar00rootroot000000 000000 Editing item: {{ name }} {% block description %} A basic description is: {{ desc }} {% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/item.html000664 000000 000000 00000000226 14012546311 026171 0ustar00rootroot000000 000000 {% extends "item-base.html" %} {% block description %} I told you, it's name is {{ name }}. It also has the description: {{ desc }}. {% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/samples/express/views/set.html000664 000000 000000 00000000035 14012546311 026024 0ustar00rootroot000000 000000 {% set username = "foooo" %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/000775 000000 000000 00000000000 14012546311 021542 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/bundle.js000775 000000 000000 00000007326 14012546311 023364 0ustar00rootroot000000 000000 #!/usr/bin/env node /* eslint-disable vars-on-top, func-names */ 'use strict'; require('module-alias/register'); var path = require('path'); var webpack = require('webpack'); var pjson = require('../package.json'); var promiseSequence = require('./lib/utils').promiseSequence; var UglifyJsPlugin = require('uglifyjs-webpack-plugin'); var TEST_ENV = (process.env.NODE_ENV === 'test'); var destDir = path.resolve(path.join( __dirname, (TEST_ENV) ? '../tests/browser' : '../browser')); function runWebpack(opts) { var type = (opts.slim) ? '(slim, only works with precompiled templates)' : ''; var ext = (opts.min) ? '.min.js' : '.js'; if (opts.slim) { ext = '-slim' + ext; } var filename = 'nunjucks' + ext; return new Promise(function(resolve, reject) { try { var config = { entry: './nunjucks/index.js', devtool: 'source-map', output: { path: destDir, filename: filename, library: 'nunjucks', libraryTarget: 'umd', devtoolModuleFilenameTemplate: function(info) { return path.relative(destDir, info.absoluteResourcePath); } }, node: { process: false, setImmediate: false }, module: { rules: [{ test: /nunjucks/, exclude: /(node_modules|browser|tests)(?!\.js)/, use: { loader: 'babel-loader', options: { plugins: [['module-resolver', { extensions: ['.js'], resolvePath: function(sourcePath) { if (sourcePath.match(/^(fs|path|chokidar)$/)) { return 'node-libs-browser/mock/empty'; } if (opts.slim) { if (sourcePath.match(/(nodes|lexer|parser|precompile|transformer|compiler)(\.js)?$/)) { return 'node-libs-browser/mock/empty'; } } if (sourcePath.match(/\/loaders(\.js)?$/)) { return sourcePath.replace('loaders', (opts.slim) ? 'precompiled-loader' : 'web-loaders'); } return null; }, }]] } } }] }, plugins: [ new webpack.BannerPlugin( 'Browser bundle of nunjucks ' + pjson.version + ' ' + type ), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 'process.env.BUILD_TYPE': JSON.stringify((opts.slim) ? 'SLIM' : 'STD'), }), ] }; if (opts.min) { config.plugins.push( new UglifyJsPlugin({ sourceMap: true, uglifyOptions: { mangle: { properties: { regex: /^_[^_]/ } }, compress: { unsafe: true } } }) ); } webpack(config).run(function(err, stats) { if (err) { reject(err); } else { resolve(stats.toString({cached: false, cachedAssets: false})); } }); } catch (err) { reject(err); } }); } var runConfigs = [ {min: true, slim: false}, {min: true, slim: true} ]; if (!TEST_ENV) { runConfigs.unshift( {min: false, slim: false}, {min: false, slim: true}); } var promises = runConfigs.map(function(opts) { return function() { return runWebpack(opts).then(function(stats) { console.log(stats); // eslint-disable-line no-console }); }; }); promiseSequence(promises).catch(function(err) { throw err; }); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/000775 000000 000000 00000000000 14012546311 022310 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/arrow-function-coverage-fix.js000664 000000 000000 00000000777 14012546311 030213 0ustar00rootroot000000 000000 // Restore old babylon behavior for istanbul. // https://github.com/babel/babel/pull/6836 // https://github.com/istanbuljs/istanbuljs/issues/119 module.exports = function hacks() { return { visitor: { Program: function Program(programPath) { programPath.traverse({ ArrowFunctionExpression: function ArrowFunctionExpression(path) { var node = path.node; node.expression = node.body.type !== 'BlockStatement'; }, }); }, }, }; }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/is-main-module.js000664 000000 000000 00000000541 14012546311 025466 0ustar00rootroot000000 000000 module.exports = function isMainModule() { // generate a stack trace var stack = (new Error()).stack; // the third line refers to our caller var stackLine = stack.split('\n')[2]; // extract the module name from that line var callerModuleName = /\((.*):\d+:\d+\)$/.exec(stackLine)[1]; return require.main.filename === callerModuleName; }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/mocha-phantomjs.js000664 000000 000000 00000003032 14012546311 025734 0ustar00rootroot000000 000000 'use strict'; var spawn = require('child_process').spawn; var path = require('path'); var lookup = require('./utils').lookup; module.exports = function mochaPhantomJS(url, options) { options = options || {}; const coverageFile = path.join( __dirname, '../../.nyc_output', (url.indexOf('slim') > -1) ? 'browser-slim.json' : 'browser-std.json'); return new Promise((resolve, reject) => { try { const scriptPath = require.resolve('mocha-phantomjs-core/mocha-phantomjs-core.js'); if (!scriptPath) { throw new Error('mocha-phantomjs-core.js not found'); } const args = [ scriptPath, url, options.reporter || 'dot', JSON.stringify(Object.assign({ useColors: true, hooks: 'mocha-phantomjs-istanbul', coverageFile: coverageFile, }, options.phantomjs || {})), ]; const phantomjsPath = lookup('.bin/phantomjs', true) || lookup('phantomjs-prebuilt/bin/phantomjs', true); if (!phantomjsPath) { throw new Error('PhantomJS not found'); } const proc = spawn(phantomjsPath, args, {cwd: path.join(__dirname, '../..')}); proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); proc.on('error', (err) => { reject(err); }); proc.on('exit', (code) => { if (code === 0) { resolve(); } else { reject(new Error('test failed. phantomjs exit code: ' + code)); } }); } catch (err) { reject(err); } }); }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/precompile.js000664 000000 000000 00000001112 14012546311 025000 0ustar00rootroot000000 000000 const fs = require('fs'); const path = require('path'); const precompile = require('../../nunjucks/src/precompile').precompile; var testDir = path.join(__dirname, '../../tests'); function precompileTestTemplates() { return new Promise((resolve, reject) => { try { const output = precompile(path.join(testDir, 'templates'), { include: [/\.(njk|html)$/], }); fs.writeFileSync(path.join(testDir, 'browser/precompiled-templates.js'), output); resolve(); } catch (err) { reject(err); } }); } module.exports = precompileTestTemplates; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/runtests.js000664 000000 000000 00000004267 14012546311 024546 0ustar00rootroot000000 000000 var mochaPhantom = require('./mocha-phantomjs'); var spawn = require('child_process').spawn; var getStaticServer = require('./static-server'); var path = require('path'); var utils = require('./utils'); var lookup = utils.lookup; var promiseSequence = utils.promiseSequence; function mochaRun({cliTest = false} = {}) { // We need to run the cli test without nyc because of weird behavior // with spawn-wrap const bin = lookup((cliTest) ? '.bin/mocha' : '.bin/nyc', true); const runArgs = (cliTest) ? [] : [ '--require', '@babel/register', '--exclude', 'tests/**', '--silent', '--no-clean', require.resolve('mocha/bin/mocha'), ]; const mochaArgs = (cliTest) ? ['tests/cli.js'] : ['--grep', 'precompile cli', '--invert', 'tests']; return new Promise((resolve, reject) => { try { const proc = spawn(bin, [ ...runArgs, '-R', 'spec', '-r', 'tests/setup', '-r', '@babel/register', ...mochaArgs, ], { cwd: path.join(__dirname, '../..'), env: process.env }); proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); proc.on('error', (err) => reject(err)); proc.on('exit', (code) => { if (code === 0) { resolve(); } else { reject(new Error('test failed. nyc/mocha exit code: ' + code)); } }); } catch (err) { reject(err); } }); } function runtests() { return new Promise((resolve, reject) => { var server; const mochaPromise = promiseSequence([ () => mochaRun({cliTest: false}), () => mochaRun({cliTest: true}), ]); return mochaPromise.then(() => { return getStaticServer().then((args) => { server = args[0]; const port = args[1]; const promises = ['index', 'slim'].map( f => (() => mochaPhantom(`http://localhost:${port}/tests/browser/${f}.html`))); return promiseSequence(promises).then(() => { server.close(); resolve(); }); }); }).catch((err) => { if (server) { server.close(); } reject(err); }); }); } module.exports = runtests; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/static-server.js000664 000000 000000 00000001527 14012546311 025446 0ustar00rootroot000000 000000 var connect = require('connect'); var getPort = require('get-port'); var serveStatic = require('serve-static'); var http = require('http'); var path = require('path'); function getStaticServer(port) { var staticRoot = path.join(__dirname, '../..'); var portPromise = (typeof port === 'undefined') ? getPort() : Promise.resolve(port); return portPromise.then((port) => { // eslint-disable-line no-shadow return new Promise((resolve, reject) => { try { const app = connect().use(serveStatic(staticRoot)); const server = http.createServer(app); server.listen(port, () => { console.log('Test server listening on port ' + port); // eslint-disable-line no-console resolve([server, port]); }); } catch (e) { reject(e); } }); }); } module.exports = getStaticServer; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/lib/utils.js000664 000000 000000 00000001616 14012546311 024012 0ustar00rootroot000000 000000 'use strict'; var fs = require('fs'); var path = require('path'); function lookup(relPath, isExecutable) { for (let i = 0; i < module.paths.length; i++) { let absPath = path.join(module.paths[i], relPath); if (isExecutable && process.platform === 'win32') { absPath += '.cmd'; } if (fs.existsSync(absPath)) { return absPath; } } return undefined; } function promiseSequence(promises) { return new Promise((resolve, reject) => { var results = []; function iterator(prev, curr) { return prev.then((result) => { results.push(result); return curr(result, results); }).catch((err) => { reject(err); }); } promises.push(() => Promise.resolve()); promises.reduce(iterator, Promise.resolve(false)).then((res) => resolve(res)); }); } module.exports = { lookup: lookup, promiseSequence: promiseSequence }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/scripts/testrunner.js000775 000000 000000 00000001217 14012546311 024315 0ustar00rootroot000000 000000 #!/usr/bin/env node 'use strict'; var NYC = require('nyc'); process.env.NODE_ENV = 'test'; const nyc = new NYC({ exclude: ['*.min.js', 'scripts/**', 'tests/**'], reporter: ['text', 'html', 'lcovonly'], showProcessTree: true }); nyc.reset(); require('@babel/register'); const runtests = require('./lib/runtests'); const precompileTestTemplates = require('./lib/precompile'); let err; precompileTestTemplates() .then(() => runtests()) .catch((e) => { err = e; console.log(err); // eslint-disable-line no-console }) .then(() => { nyc.writeCoverageFile(); nyc.report(); if (err) { process.exit(1); } }); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/000775 000000 000000 00000000000 14012546311 021215 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/.eslintrc.js000664 000000 000000 00000001346 14012546311 023460 0ustar00rootroot000000 000000 module.exports = { 'env': { node: true, es6: false, mocha: true, browser: true, }, 'rules': { // func-names is annoying when you don't have arrow syntax 'func-names': 'off', // To deal with browser environments, we need to have require // calls inside a conditional 'global-require': 'off', 'spaced-comment': ['error', 'always', { 'exceptions': ['*', ','] }], // This is another rule that doesn't make sense when you don't have block-level // variable declarations 'one-var': 'off', 'one-var-declaration-per-line': 'off', // We need tests to run without babel on browsers that might not have Object.keys // and related functions 'no-restricted-syntax': 'off', }, }; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/api.js000664 000000 000000 00000005426 14012546311 022333 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect; var util; var Environment; var Loader; var templatesPath; var path; if (typeof require !== 'undefined') { expect = require('expect.js'); util = require('./util'); Environment = require('../nunjucks/src/environment').Environment; Loader = require('../nunjucks/src/node-loaders').FileSystemLoader; templatesPath = 'tests/templates'; path = require('path'); } else { expect = window.expect; Environment = nunjucks.Environment; Loader = nunjucks.WebLoader; templatesPath = '../templates'; } describe('api', function() { it('should always force compilation of parent template', function() { var env = new Environment(new Loader(templatesPath)); var child = env.getTemplate('base-inherit.njk'); expect(child.render()).to.be('Foo*Bar*BazFizzle'); }); it('should only call the callback once when conditional import fails', function(done) { var env = new Environment(new Loader(templatesPath)); var called = 0; env.render('broken-conditional-include.njk', function() { expect(++called).to.be(1); } ); setTimeout(done, 0); }); it('should handle correctly relative paths', function() { var env; var child1; var child2; if (typeof path === 'undefined') { this.skip(); return; } env = new Environment(new Loader(templatesPath)); child1 = env.getTemplate('relative/test1.njk'); child2 = env.getTemplate('relative/test2.njk'); expect(child1.render()).to.be('FooTest1BazFizzle'); expect(child2.render()).to.be('FooTest2BazFizzle'); }); it('should handle correctly cache for relative paths', function() { var env; var test; if (typeof path === 'undefined') { this.skip(); return; } env = new Environment(new Loader(templatesPath)); test = env.getTemplate('relative/test-cache.njk'); expect(util.normEOL(test.render())).to.be('Test1\nTest2'); }); it('should handle correctly relative paths in renderString', function() { var env; if (typeof path === 'undefined') { this.skip(); return; } env = new Environment(new Loader(templatesPath)); expect(env.renderString('{% extends "./relative/test1.njk" %}{% block block1 %}Test3{% endblock %}', {}, { path: path.resolve(templatesPath, 'string.njk') })).to.be('FooTest3BazFizzle'); }); it('should emit "load" event on Environment instance', function(done) { var env = new Environment(new Loader(templatesPath)); env.on('load', function(name, source) { expect(name).to.equal('item.njk'); done(); }); env.render('item.njk', {}); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/browser/000775 000000 000000 00000000000 14012546311 022700 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/browser/index.html000664 000000 000000 00000003441 14012546311 024677 0ustar00rootroot000000 000000 Mocha Tests
Tests do not indicate performance. They are much slower because a new environment is created for every single template and is recompiled from scratch, and you can see there are quite a lot of tests.
nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/browser/slim.html000664 000000 000000 00000003523 14012546311 024535 0ustar00rootroot000000 000000 Mocha Tests (slim)
nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/cli.js000664 000000 000000 00000002334 14012546311 022324 0ustar00rootroot000000 000000 (function() { 'use strict'; var path = require('path'); var execFile = require('child_process').execFile; var expect = require('expect.js'); var rootDir = path.resolve(path.join(__dirname, '..')); var precompileBin = path.join(rootDir, 'bin', 'precompile'); if (process.platform === 'win32') { precompileBin += '.cmd'; } function execPrecompile(args, cb) { execFile(precompileBin, args, {cwd: rootDir}, cb); } describe('precompile cli', function() { it('should echo a compiled template to stdout', function(done) { execPrecompile(['tests/templates/item.njk'], function(err, stdout, stderr) { if (err) { done(err); return; } expect(stdout).to.contain('window.nunjucksPrecompiled'); expect(stderr).to.equal(''); done(); }); }); it('should support --name', function(done) { var args = [ '--name', 'item.njk', 'tests/templates/item.njk', ]; execPrecompile(args, function(err, stdout, stderr) { if (err) { done(err); return; } expect(stdout).to.contain('"item.njk"'); expect(stderr).to.equal(''); done(); }); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/compiler.js000664 000000 000000 00000176636 14012546311 023410 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect; var util; var Template; var Loader; var Environment; var fs; var render; var equal; var finish; var isSlim; if (typeof require !== 'undefined') { expect = require('expect.js'); util = require('./util'); Template = require('../nunjucks/src/environment').Template; Environment = require('../nunjucks/src/environment').Environment; fs = require('fs'); } else { expect = window.expect; util = window.util; Template = nunjucks.Template; Environment = nunjucks.Environment; } render = util.render; equal = util.equal; finish = util.finish; isSlim = util.isSlim; Loader = util.Loader; describe('compiler', function() { it('should compile templates', function(done) { equal('Hello world', 'Hello world'); equal('Hello world, {{ name }}', { name: 'James' }, 'Hello world, James'); equal('Hello world, {{name}}{{suffix}}, how are you', { name: 'James', suffix: ' Long' }, 'Hello world, James Long, how are you'); finish(done); }); it('should escape newlines', function(done) { equal('foo\\nbar', 'foo\\nbar'); finish(done); }); it('should escape Unicode line seperators', function(done) { equal('\u2028', '\u2028'); finish(done); }); it('should compile references', function(done) { equal('{{ foo.bar }}', { foo: { bar: 'baz' } }, 'baz'); equal('{{ foo["bar"] }}', { foo: { bar: 'baz' } }, 'baz'); finish(done); }); it('should compile references - object without prototype', function(done) { var context = Object.create(null); context.foo = Object.create(null); context.foo.bar = 'baz'; equal('{{ foo.bar }}', context, 'baz'); equal('{{ foo["bar"] }}', context, 'baz'); finish(done); }); it('should fail silently on undefined values', function(done) { equal('{{ foo }}', ''); equal('{{ foo.bar }}', ''); equal('{{ foo.bar.baz }}', ''); equal('{{ foo.bar.baz["biz"].mumble }}', ''); finish(done); }); it('should not treat falsy values the same as undefined', function(done) { equal('{{ foo }}', { foo: 0 }, '0'); equal('{{ foo }}', { foo: false }, 'false'); finish(done); }); it('should display none as empty string', function(done) { equal('{{ none }}', ''); finish(done); }); it('should compile none as falsy', function(done) { equal('{% if not none %}yes{% endif %}', 'yes'); finish(done); }); it('should compile none as null, not undefined', function(done) { equal('{{ none|default("d", false) }}', ''); finish(done); }); it('should compile function calls', function(done) { equal('{{ foo("msg") }}', { foo: function(str) { return str + 'hi'; } }, 'msghi'); finish(done); }); it('should compile function calls with correct scope', function(done) { equal('{{ foo.bar() }}', { foo: { bar: function() { return this.baz; }, baz: 'hello' } }, 'hello'); finish(done); }); it('should compile switch statements', function() { // standard switches var tpl1 = '{% switch foo %}{% case "bar" %}BAR{% case "baz" %}BAZ{% default %}NEITHER FOO NOR BAR{% endswitch %}'; // test no-default switches var tpl2 = '{% switch foo %}{% case "bar" %}BAR{% case "baz" %}BAZ{% endswitch %}'; // test fall-through cases var tpl3 = '{% switch foo %}{% case "bar" %}{% case "baz" %}BAR{% endswitch %}'; equal(tpl1, 'NEITHER FOO NOR BAR'); equal(tpl1, { foo: 'bar' }, 'BAR'); equal(tpl1, { foo: 'baz' }, 'BAZ'); equal(tpl2, ''); equal(tpl3, { foo: 'bar' }, 'BAR'); equal(tpl3, { foo: 'baz' }, 'BAR'); }); it('should compile if blocks', function(done) { var tmpl = ('Give me some {% if hungry %}pizza' + '{% else %}water{% endif %}'); equal(tmpl, { hungry: true }, 'Give me some pizza'); equal(tmpl, { hungry: false }, 'Give me some water'); equal('{% if not hungry %}good{% endif %}', { hungry: false }, 'good'); equal('{% if hungry and like_pizza %}good{% endif %}', { hungry: true, like_pizza: true }, 'good'); equal('{% if hungry or like_pizza %}good{% endif %}', { hungry: false, like_pizza: true }, 'good'); equal('{% if (hungry or like_pizza) and anchovies %}good{% endif %}', { hungry: false, like_pizza: true, anchovies: true }, 'good'); equal( '{% if food == "pizza" %}pizza{% endif %}' + '{% if food =="beer" %}beer{% endif %}', { food: 'beer' }, 'beer'); equal('{% if "pizza" in food %}yum{% endif %}', { food: { pizza: true } }, 'yum'); equal('{% if pizza %}yum{% elif anchovies %}yuck{% endif %}', { pizza: true }, 'yum'); equal('{% if pizza %}yum{% elseif anchovies %}yuck{% endif %}', { pizza: true }, 'yum'); equal('{% if pizza %}yum{% elif anchovies %}yuck{% endif %}', { anchovies: true }, 'yuck'); equal('{% if pizza %}yum{% elseif anchovies %}yuck{% endif %}', { anchovies: true }, 'yuck'); equal( '{% if topping == "pepperoni" %}yum{% elseif topping == "anchovies" %}' + 'yuck{% else %}hmmm{% endif %}', { topping: 'sausage' }, 'hmmm'); finish(done); }); it('should compile the ternary operator', function(done) { equal('{{ "foo" if bar else "baz" }}', 'baz'); equal('{{ "foo" if bar else "baz" }}', { bar: true }, 'foo'); finish(done); }); it('should compile inline conditionals', function(done) { var tmpl = 'Give me some {{ "pizza" if hungry else "water" }}'; equal(tmpl, { hungry: true }, 'Give me some pizza'); equal(tmpl, { hungry: false }, 'Give me some water'); equal('{{ "good" if not hungry }}', { hungry: false }, 'good'); equal('{{ "good" if hungry and like_pizza }}', { hungry: true, like_pizza: true }, 'good'); equal('{{ "good" if hungry or like_pizza }}', { hungry: false, like_pizza: true }, 'good'); equal('{{ "good" if (hungry or like_pizza) and anchovies }}', { hungry: false, like_pizza: true, anchovies: true }, 'good'); equal( '{{ "pizza" if food == "pizza" }}' + '{{ "beer" if food == "beer" }}', { food: 'beer' }, 'beer'); finish(done); }); function runLoopTests(block) { var end = { asyncAll: 'endall', asyncEach: 'endeach', for: 'endfor' }[block]; describe('the ' + block + ' tag', function() { it('should loop over simple arrays', function() { equal( '{% ' + block + ' i in arr %}{{ i }}{% ' + end + ' %}', { arr: [1, 2, 3, 4, 5] }, '12345'); }); it('should loop normally with an {% else %} tag and non-empty array', function() { equal( '{% ' + block + ' i in arr %}{{ i }}{% else %}empty{% ' + end + ' %}', { arr: [1, 2, 3, 4, 5] }, '12345'); }); it('should execute the {% else %} block when looping over an empty array', function() { equal( '{% ' + block + ' i in arr %}{{ i }}{% else %}empty{% ' + end + ' %}', { arr: [] }, 'empty'); }); it('should support destructured looping', function() { equal( '{% ' + block + ' a, b, c in arr %}' + '{{ a }},{{ b }},{{ c }}.{% ' + end + ' %}', { arr: [['x', 'y', 'z'], ['1', '2', '3']] }, 'x,y,z.1,2,3.'); }); it('should do loop over key-values of a literal in-template Object', function() { equal( '{% ' + block + ' k, v in { one: 1, two: 2 } %}' + '-{{ k }}:{{ v }}-{% ' + end + ' %}', '-one:1--two:2-'); }); it('should support loop.index', function() { equal('{% ' + block + ' i in [7,3,6] %}{{ loop.index }}{% ' + end + ' %}', '123'); }); it('should support loop.index0', function() { equal('{% ' + block + ' i in [7,3,6] %}{{ loop.index0 }}{% ' + end + ' %}', '012'); }); it('should support loop.revindex', function() { equal('{% ' + block + ' i in [7,3,6] %}{{ loop.revindex }}{% ' + end + ' %}', '321'); }); it('should support loop.revindex0', function() { equal('{% ' + block + ' i in [7,3,6] %}{{ loop.revindex0 }}{% ' + end + ' %}', '210'); }); it('should support loop.first', function() { equal( '{% ' + block + ' i in [7,3,6] %}' + '{% if loop.first %}{{ i }}{% endif %}' + '{% ' + end + ' %}', '7'); }); it('should support loop.last', function() { equal( '{% ' + block + ' i in [7,3,6] %}' + '{% if loop.last %}{{ i }}{% endif %}' + '{% ' + end + ' %}', '6'); }); it('should support loop.length', function() { equal('{% ' + block + ' i in [7,3,6] %}{{ loop.length }}{% ' + end + ' %}', '333'); }); it('should fail silently when looping over an undefined variable', function() { equal('{% ' + block + ' i in foo %}{{ i }}{% ' + end + ' %}', ''); }); it('should fail silently when looping over an undefined property', function() { equal( '{% ' + block + ' i in foo.bar %}{{ i }}{% ' + end + ' %}', { foo: {} }, ''); }); // TODO: this behavior differs from jinja2 it('should fail silently when looping over a null variable', function() { equal( '{% ' + block + ' i in foo %}{{ i }}{% ' + end + ' %}', { foo: null }, ''); }); it('should loop over two-dimensional arrays', function() { equal('{% ' + block + ' x, y in points %}[{{ x }},{{ y }}]{% ' + end + ' %}', { points: [[1, 2], [3, 4], [5, 6]] }, '[1,2][3,4][5,6]'); }); it('should loop over four-dimensional arrays', function() { equal( '{% ' + block + ' a, b, c, d in arr %}[{{ a }},{{ b }},{{ c }},{{ d }}]{% ' + end + '%}', { arr: [[1, 2, 3, 4], [5, 6, 7, 8]] }, '[1,2,3,4][5,6,7,8]'); }); it('should support loop.index with two-dimensional loops', function() { equal('{% ' + block + ' x, y in points %}{{ loop.index }}{% ' + end + ' %}', { points: [[1, 2], [3, 4], [5, 6]] }, '123'); }); it('should support loop.revindex with two-dimensional loops', function() { equal('{% ' + block + ' x, y in points %}{{ loop.revindex }}{% ' + end + ' %}', { points: [[1, 2], [3, 4], [5, 6]] }, '321'); }); it('should support key-value looping over an Object variable', function() { equal('{% ' + block + ' k, v in items %}({{ k }},{{ v }}){% ' + end + ' %}', { items: { foo: 1, bar: 2 } }, '(foo,1)(bar,2)'); }); it('should support loop.index when looping over an Object\'s key-value pairs', function() { equal('{% ' + block + ' k, v in items %}{{ loop.index }}{% ' + end + ' %}', { items: { foo: 1, bar: 2 } }, '12'); }); it('should support loop.revindex when looping over an Object\'s key-value pairs', function() { equal('{% ' + block + ' k, v in items %}{{ loop.revindex }}{% ' + end + ' %}', { items: { foo: 1, bar: 2 } }, '21'); }); it('should support loop.length when looping over an Object\'s key-value pairs', function() { equal('{% ' + block + ' k, v in items %}{{ loop.length }}{% ' + end + ' %}', { items: { foo: 1, bar: 2 } }, '22'); }); it('should support include tags in the body of the loop', function() { equal('{% ' + block + ' item, v in items %}{% include "item.njk" %}{% ' + end + ' %}', { items: { foo: 1, bar: 2 } }, 'showing fooshowing bar'); }); it('should work with {% set %} and {% include %} tags', function() { equal( '{% set item = passed_var %}' + '{% include "item.njk" %}\n' + '{% ' + block + ' i in passed_iter %}' + '{% set item = i %}' + '{% include "item.njk" %}\n' + '{% ' + end + ' %}', { passed_var: 'test', passed_iter: ['1', '2', '3'] }, 'showing test\nshowing 1\nshowing 2\nshowing 3\n'); }); /* global Set */ it('should work with Set builtin', function() { if (typeof Set === 'undefined') { this.skip(); } else { equal('{% ' + block + ' i in set %}{{ i }}{% ' + end + ' %}', { set: new Set([1, 2, 3, 4, 5]) }, '12345'); equal('{% ' + block + ' i in set %}{{ i }}{% else %}empty{% ' + end + ' %}', { set: new Set([1, 2, 3, 4, 5]) }, '12345'); equal('{% ' + block + ' i in set %}{{ i }}{% else %}empty{% ' + end + ' %}', { set: new Set() }, 'empty'); } }); /* global Map */ it('should work with Map builtin', function() { if (typeof Map === 'undefined') { this.skip(); } else { equal('{% ' + block + ' k, v in map %}[{{ k }},{{ v }}]{% ' + end + ' %}', { map: new Map([[1, 2], [3, 4], [5, 6]]) }, '[1,2][3,4][5,6]'); equal('{% ' + block + ' k, v in map %}[{{ k }},{{ v }}]{% else %}empty{% ' + end + ' %}', { map: new Map([[1, 2], [3, 4], [5, 6]]) }, '[1,2][3,4][5,6]'); equal('{% ' + block + ' k, v in map %}[{{ k }},{{ v }}]{% else %}empty{% ' + end + ' %}', { map: new Map() }, 'empty'); } }); }); } runLoopTests('for'); runLoopTests('asyncEach'); runLoopTests('asyncAll'); it('should allow overriding var with none inside nested scope', function(done) { equal( '{% set var = "foo" %}' + '{% for i in [1] %}{% set var = none %}{{ var }}{% endfor %}', ''); finish(done); }); it('should compile async control', function(done) { var opts; if (!fs) { this.skip(); } else { opts = { asyncFilters: { getContents: function(tmpl, cb) { fs.readFile(tmpl, cb); }, getContentsArr: function(arr, cb) { fs.readFile(arr[0], function(err, res) { cb(err, [res]); }); } } }; render('{{ tmpl | getContents }}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere'); }); render('{% if tmpl %}{{ tmpl | getContents }}{% endif %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere'); }); render('{% if tmpl | getContents %}yes{% endif %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('yes'); }); render('{% for t in [tmpl, tmpl] %}{{ t | getContents }}*{% endfor %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere*somecontenthere*'); }); render('{% for t in [tmpl, tmpl] | getContentsArr %}{{ t }}{% endfor %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere'); }); render('{% if test %}{{ tmpl | getContents }}{% endif %}oof', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('oof'); }); render( '{% if tmpl %}' + '{% for i in [0, 1] %}{{ tmpl | getContents }}*{% endfor %}' + '{% endif %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere*somecontenthere*'); }); render('{% block content %}{{ tmpl | getContents }}{% endblock %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere'); }); render('{% block content %}hello{% endblock %} {{ tmpl | getContents }}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('hello somecontenthere'); }); render('{% block content %}{% set foo = tmpl | getContents %}{{ foo }}{% endblock %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere'); }); render('{% block content %}{% include "async.njk" %}{% endblock %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere\n'); }); render('{% asyncEach i in [0, 1] %}{% include "async.njk" %}{% endeach %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('somecontenthere\nsomecontenthere\n'); }); render('{% asyncAll i in [0, 1, 2, 3, 4] %}-{{ i }}:{% include "async.njk" %}-{% endall %}', { tmpl: 'tests/templates/for-async-content.njk' }, opts, function(err, res) { expect(res).to.be('-0:somecontenthere\n-' + '-1:somecontenthere\n-' + '-2:somecontenthere\n-' + '-3:somecontenthere\n-' + '-4:somecontenthere\n-'); }); } finish(done); }); it('should compile basic arithmetic operators', function() { equal('{{ 3 + 4 - 5 * 6 / 10 }}', '4'); }); it('should compile the exponentiation (**) operator', function() { equal('{{ 4**5 }}', '1024'); }); it('should compile the integer division (//) operator', function() { equal('{{ 9//5 }}', '1'); }); it('should compile the modulus operator', function() { equal('{{ 9%5 }}', '4'); }); it('should compile numeric negation operator', function() { equal('{{ -5 }}', '-5'); }); it('should compile comparison operators', function() { equal('{% if 3 < 4 %}yes{% endif %}', 'yes'); equal('{% if 3 > 4 %}yes{% endif %}', ''); equal('{% if 9 >= 10 %}yes{% endif %}', ''); equal('{% if 10 >= 10 %}yes{% endif %}', 'yes'); equal('{% if 9 <= 10 %}yes{% endif %}', 'yes'); equal('{% if 10 <= 10 %}yes{% endif %}', 'yes'); equal('{% if 11 <= 10 %}yes{% endif %}', ''); equal('{% if 10 != 10 %}yes{% endif %}', ''); equal('{% if 10 == 10 %}yes{% endif %}', 'yes'); equal('{% if "0" == 0 %}yes{% endif %}', 'yes'); equal('{% if "0" === 0 %}yes{% endif %}', ''); equal('{% if "0" !== 0 %}yes{% endif %}', 'yes'); equal('{% if 0 == false %}yes{% endif %}', 'yes'); equal('{% if 0 === false %}yes{% endif %}', ''); equal('{% if foo(20) > bar %}yes{% endif %}', { foo: function(n) { return n - 1; }, bar: 15 }, 'yes'); }); it('should compile python-style ternary operators', function() { equal('{{ "yes" if 1 is odd else "no" }}', 'yes'); equal('{{ "yes" if 2 is even else "no" }}', 'yes'); equal('{{ "yes" if 2 is odd else "no" }}', 'no'); equal('{{ "yes" if 1 is even else "no" }}', 'no'); }); it('should compile the "in" operator for Arrays', function() { equal('{% if 1 in [1, 2] %}yes{% endif %}', 'yes'); equal('{% if 1 in [2, 3] %}yes{% endif %}', ''); equal('{% if 1 not in [1, 2] %}yes{% endif %}', ''); equal('{% if 1 not in [2, 3] %}yes{% endif %}', 'yes'); equal('{% if "a" in vals %}yes{% endif %}', { vals: ['a', 'b'] }, 'yes'); }); it('should compile the "in" operator for objects', function() { equal('{% if "a" in obj %}yes{% endif %}', { obj: { a: true } }, 'yes'); equal('{% if "a" in obj %}yes{% endif %}', { obj: { b: true } }, ''); }); it('should compile the "in" operator for strings', function() { equal('{% if "foo" in "foobar" %}yes{% endif %}', 'yes'); }); it('should throw an error when using the "in" operator on unexpected types', function(done) { render( '{% if "a" in 1 %}yes{% endif %}', {}, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match( /Cannot use "in" operator to search for "a" in unexpected types\./ ); } ); render( '{% if "a" in obj %}yes{% endif %}', {}, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match( /Cannot use "in" operator to search for "a" in unexpected types\./ ); } ); finish(done); }); if (!isSlim) { it('should throw exceptions when called synchronously', function() { var tmpl = new Template('{% from "doesnotexist" import foo %}'); function templateRender() { tmpl.render(); } expect(templateRender).to.throwException(/template not found: doesnotexist/); }); it('should include error line in raised TemplateError', function(done) { var tmplStr = [ '{% set items = ["a", "b",, "c"] %}', '{{ items | join(",") }}', ].join('\n'); var loader = new Loader('tests/templates'); var env = new Environment(loader); var tmpl = new Template(tmplStr, env, 'parse-error.njk'); tmpl.render({}, function(err, res) { expect(res).to.be(undefined); expect(err.toString()).to.be([ 'Template render error: (parse-error.njk) [Line 1, Column 26]', ' unexpected token: ,', ].join('\n')); done(); }); }); it('should include error line when exception raised in user function', function(done) { var tmplStr = [ '{% block content %}', '
{{ foo() }}
', '{% endblock %}', ].join('\n'); var env = new Environment(new Loader('tests/templates')); var tmpl = new Template(tmplStr, env, 'user-error.njk'); function foo() { throw new Error('ERROR'); } tmpl.render({foo: foo}, function(err, res) { expect(res).to.be(undefined); expect(err.toString()).to.be([ 'Template render error: (user-error.njk) [Line 1, Column 11]', ' Error: ERROR', ].join('\n')); done(); }); }); } it('should throw exceptions from included templates when called synchronously', function() { function templateRender() { render('{% include "broken-import.njk" %}', {str: 'abc'}); } expect(templateRender).to.throwException(/template not found: doesnotexist/); }); it('should pass errors from included templates to callback when async', function(done) { render( '{% include "broken-import.njk" %}', {str: 'abc'}, {noThrow: true}, function(err, res) { expect(err).to.match(/template not found: doesnotexist/); expect(res).to.be(undefined); done(); }); }); it('should compile string concatenations with tilde', function(done) { equal('{{ 4 ~ \'hello\' }}', '4hello'); equal('{{ 4 ~ 5 }}', '45'); equal('{{ \'a\' ~ \'b\' ~ 5 }}', 'ab5'); finish(done); }); it('should compile macros', function(done) { equal( '{% macro foo() %}This is a macro{% endmacro %}' + '{{ foo() }}', 'This is a macro'); finish(done); }); it('should compile macros with optional args', function(done) { equal( '{% macro foo(x, y) %}{{ y }}{% endmacro %}' + '{{ foo(1) }}', ''); finish(done); }); it('should compile macros with args that can be passed to filters', function(done) { equal( '{% macro foo(x) %}{{ x|title }}{% endmacro %}' + '{{ foo("foo") }}', 'Foo'); finish(done); }); it('should compile macros with positional args', function(done) { equal( '{% macro foo(x, y) %}{{ y }}{% endmacro %}' + '{{ foo(1, 2) }}', '2'); finish(done); }); it('should compile macros with arg defaults', function(done) { equal( '{% macro foo(x, y, z=5) %}{{ y }}{% endmacro %}' + '{{ foo(1, 2) }}', '2'); equal( '{% macro foo(x, y, z=5) %}{{ z }}{% endmacro %}' + '{{ foo(1, 2) }}', '5'); finish(done); }); it('should compile macros with keyword args', function(done) { equal( '{% macro foo(x, y, z=5) %}{{ y }}{% endmacro %}' + '{{ foo(1, y=2) }}', '2'); finish(done); }); it('should compile macros with only keyword args', function(done) { equal( '{% macro foo(x, y, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{{ foo(x=1, y=2) }}', '125'); finish(done); }); it('should compile macros with keyword args overriding defaults', function(done) { equal( '{% macro foo(x, y, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{{ foo(x=1, y=2, z=3) }}', '123'); finish(done); }); it('should compile macros with out-of-order keyword args', function(done) { equal( '{% macro foo(x, y=2, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{{ foo(1, z=3) }}', '123'); finish(done); }); it('should compile macros', function(done) { equal( '{% macro foo(x, y=2, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{{ foo(1) }}', '125'); finish(done); }); it('should compile macros with multiple overridden arg defaults', function(done) { equal( '{% macro foo(x, y=2, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{{ foo(1, 10, 20) }}', '11020'); finish(done); }); it('should compile macro calls inside blocks', function(done) { equal( '{% extends "base.njk" %}' + '{% macro foo(x, y=2, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{% block block1 %}' + '{{ foo(1) }}' + '{% endblock %}', 'Foo125BazFizzle'); finish(done); }); it('should compile macros defined in one block and called in another', function(done) { equal( '{% block bar %}' + '{% macro foo(x, y=2, z=5) %}{{ x }}{{ y }}{{ z }}' + '{% endmacro %}' + '{% endblock %}' + '{% block baz %}' + '{{ foo(1) }}' + '{% endblock %}', '125'); finish(done); }); it('should compile macros that include other templates', function(done) { equal( '{% macro foo() %}{% include "include.njk" %}{% endmacro %}' + '{{ foo() }}', { name: 'james' }, 'FooInclude james'); finish(done); }); it('should compile macros that set vars', function(done) { equal( '{% macro foo() %}{% set x = "foo"%}{{ x }}{% endmacro %}' + '{% set x = "bar" %}' + '{{ x }}' + '{{ foo() }}' + '{{ x }}', 'barfoobar'); finish(done); }); it('should not leak variables set in macro to calling scope', function(done) { equal( '{% macro setFoo() %}' + '{% set x = "foo" %}' + '{{ x }}' + '{% endmacro %}' + '{% macro display() %}' + '{% set x = "bar" %}' + '{{ setFoo() }}' + '{{ x }}' + '{% endmacro %}' + '{{ display() }}', 'foobar'); finish(done); }); it('should not leak variables set in nested scope within macro out to calling scope', function(done) { equal( '{% macro setFoo() %}' + '{% for y in [1] %}{% set x = "foo" %}{{ x }}{% endfor %}' + '{% endmacro %}' + '{% macro display() %}' + '{% set x = "bar" %}' + '{{ setFoo() }}' + '{{ x }}' + '{% endmacro %}' + '{{ display() }}', 'foobar'); finish(done); }); it('should compile macros without leaking set to calling scope', function(done) { // This test checks that the issue #577 is resolved. // If the bug is not fixed, and set variables leak into the // caller scope, there will be too many "foo"s here ("foofoofoo"), // because each recursive call will append a "foo" to the // variable x in its caller's scope, instead of just its own. equal( '{% macro foo(topLevel, prefix="") %}' + '{% if topLevel %}' + '{% set x = "" %}' + '{% for i in [1,2] %}' + '{{ foo(false, x) }}' + '{% endfor %}' + '{% else %}' + '{% set x = prefix + "foo" %}' + '{{ x }}' + '{% endif %}' + '{% endmacro %}' + '{{ foo(true) }}', 'foofoo'); finish(done); }); it('should compile macros that cannot see variables in caller scope', function(done) { equal( '{% macro one(var) %}{{ two() }}{% endmacro %}' + '{% macro two() %}{{ var }}{% endmacro %}' + '{{ one("foo") }}', ''); finish(done); }); it('should compile call blocks', function(done) { equal( '{% macro wrap(el) %}' + '<{{ el }}>{{ caller() }}' + '{% endmacro %}' + '{% call wrap("div") %}Hello{% endcall %}', '
Hello
'); finish(done); }); it('should compile call blocks with args', function(done) { equal( '{% macro list(items) %}' + '
    {% for i in items %}' + '
  • {{ caller(i) }}
  • ' + '{% endfor %}
' + '{% endmacro %}' + '{% call(item) list(["a", "b"]) %}{{ item }}{% endcall %}', '
  • a
  • b
'); finish(done); }); it('should compile call blocks using imported macros', function(done) { equal( '{% import "import.njk" as imp %}' + '{% call imp.wrap("span") %}Hey{% endcall %}', 'Hey'); finish(done); }); it('should import templates', function(done) { equal( '{% import "import.njk" as imp %}' + '{{ imp.foo() }} {{ imp.bar }}', 'Here\'s a macro baz'); equal( '{% from "import.njk" import foo as baz, bar %}' + '{{ bar }} {{ baz() }}', 'baz Here\'s a macro'); // TODO: Should the for loop create a new frame for each // iteration? As it is, `num` is set on all iterations after // the first one sets it equal( '{% for i in [1,2] %}' + 'start: {{ num }}' + '{% from "import.njk" import bar as num %}' + 'end: {{ num }}' + '{% endfor %}' + 'final: {{ num }}', 'start: end: bazstart: bazend: bazfinal: '); finish(done); }); it('should import templates with context', function(done) { equal( '{% set bar = "BAR" %}' + '{% import "import-context.njk" as imp with context %}' + '{{ imp.foo() }}', 'Here\'s BAR'); equal( '{% set bar = "BAR" %}' + '{% from "import-context.njk" import foo with context %}' + '{{ foo() }}', 'Here\'s BAR'); equal( '{% set bar = "BAR" %}' + '{% import "import-context-set.njk" as imp %}' + '{{ bar }}', 'BAR'); equal( '{% set bar = "BAR" %}' + '{% import "import-context-set.njk" as imp %}' + '{{ imp.bar }}', 'FOO'); equal( '{% set bar = "BAR" %}' + '{% import "import-context-set.njk" as imp with context %}' + '{{ bar }}{{ buzz }}', 'FOO'); equal( '{% set bar = "BAR" %}' + '{% import "import-context-set.njk" as imp with context %}' + '{{ imp.bar }}{{ buzz }}', 'FOO'); finish(done); }); it('should import templates without context', function(done) { equal( '{% set bar = "BAR" %}' + '{% import "import-context.njk" as imp without context %}' + '{{ imp.foo() }}', 'Here\'s '); equal( '{% set bar = "BAR" %}' + '{% from "import-context.njk" import foo without context %}' + '{{ foo() }}', 'Here\'s '); finish(done); }); it('should default to importing without context', function(done) { equal( '{% set bar = "BAR" %}' + '{% import "import-context.njk" as imp %}' + '{{ imp.foo() }}', 'Here\'s '); equal( '{% set bar = "BAR" %}' + '{% from "import-context.njk" import foo %}' + '{{ foo() }}', 'Here\'s '); finish(done); }); it('should inherit templates', function(done) { equal('{% extends "base.njk" %}', 'FooBarBazFizzle'); equal('hola {% extends "base.njk" %} hizzle mumble', 'FooBarBazFizzle'); equal('{% extends "base.njk" %}{% block block1 %}BAR{% endblock %}', 'FooBARBazFizzle'); equal( '{% extends "base.njk" %}' + '{% block block1 %}BAR{% endblock %}' + '{% block block2 %}BAZ{% endblock %}', 'FooBARBAZFizzle'); equal('hola {% extends tmpl %} hizzle mumble', { tmpl: 'base.njk' }, 'FooBarBazFizzle'); finish(done); }); it('should not call blocks not defined from template inheritance', function(done) { var count = 0; render( '{% extends "base.njk" %}' + '{% block notReal %}{{ foo() }}{% endblock %}', { foo: function() { count++; } }, function() { expect(count).to.be(0); }); finish(done); }); it('should conditionally inherit templates', function(done) { equal( '{% if false %}{% extends "base.njk" %}{% endif %}' + '{% block block1 %}BAR{% endblock %}', 'BAR'); equal( '{% if true %}{% extends "base.njk" %}{% endif %}' + '{% block block1 %}BAR{% endblock %}', 'FooBARBazFizzle'); equal( '{% if true %}' + '{% extends "base.njk" %}' + '{% else %}' + '{% extends "base2.njk" %}' + '{% endif %}' + '{% block block1 %}HELLO{% endblock %}', 'FooHELLOBazFizzle'); equal( '{% if false %}' + '{% extends "base.njk" %}' + '{% else %}' + '{% extends "base2.njk" %}' + '{% endif %}' + '{% block item %}hello{{ item }}{% endblock %}', 'hello1hello2'); finish(done); }); it('should error if same block is defined multiple times', function(done) { var func = function() { render( '{% extends "simple-base.njk" %}' + '{% block test %}{% endblock %}' + '{% block test %}{% endblock %}'); }; expect(func).to.throwException(/Block "test" defined more than once./); finish(done); }); it('should render nested blocks in child template', function(done) { equal( '{% extends "base.njk" %}' + '{% block block1 %}{% block nested %}BAR{% endblock %}{% endblock %}', 'FooBARBazFizzle'); finish(done); }); it('should render parent blocks with super()', function(done) { equal( '{% extends "base.njk" %}' + '{% block block1 %}{{ super() }}BAR{% endblock %}', 'FooBarBARBazFizzle'); // two levels of `super` should work equal( '{% extends "base-inherit.njk" %}' + '{% block block1 %}*{{ super() }}*{% endblock %}', 'Foo**Bar**BazFizzle'); finish(done); }); it('should let super() see global vars from child template', function(done) { equal( '{% extends "base-show.njk" %}{% set var = "child" %}' + '{% block main %}{{ super() }}{% endblock %}', 'child'); finish(done); }); it('should not let super() see vars from child block', function(done) { equal( '{% extends "base-show.njk" %}' + '{% block main %}{% set var = "child" %}{{ super() }}{% endblock %}', ''); finish(done); }); it('should let child templates access parent global scope', function(done) { equal( '{% extends "base-set.njk" %}' + '{% block main %}{{ var }}{% endblock %}', 'parent'); finish(done); }); it('should not let super() modify calling scope', function(done) { equal( '{% extends "base-set-inside-block.njk" %}' + '{% block main %}{{ super() }}{{ var }}{% endblock %}', ''); finish(done); }); it('should not let child templates set vars in parent scope', function(done) { equal( '{% extends "base-set-and-show.njk" %}' + '{% block main %}{% set var = "child" %}{% endblock %}', 'parent'); finish(done); }); it('should render blocks in their own scope', function(done) { equal( '{% set var = "parent" %}' + '{% block main %}{% set var = "inner" %}{% endblock %}' + '{{ var }}', 'parent'); finish(done); }); it('should include templates', function(done) { equal('hello world {% include "include.njk" %}', 'hello world FooInclude '); finish(done); }); it('should include 130 templates without call stack size exceed', function(done) { equal('{% include "includeMany.njk" %}', new Array(131).join('FooInclude \n')); finish(done); }); it('should include templates with context', function(done) { equal('hello world {% include "include.njk" %}', { name: 'james' }, 'hello world FooInclude james'); finish(done); }); it('should include templates that can see including scope, but not write to it', function(done) { equal('{% set var = 1 %}{% include "include-set.njk" %}{{ var }}', '12\n1'); finish(done); }); it('should include templates dynamically', function(done) { equal('hello world {% include tmpl %}', { name: 'thedude', tmpl: 'include.njk' }, 'hello world FooInclude thedude'); finish(done); }); it('should include templates dynamically based on a set var', function(done) { equal('hello world {% set tmpl = "include.njk" %}{% include tmpl %}', { name: 'thedude' }, 'hello world FooInclude thedude'); finish(done); }); it('should include templates dynamically based on an object attr', function(done) { equal('hello world {% include data.tmpl %}', { name: 'thedude', data: { tmpl: 'include.njk' } }, 'hello world FooInclude thedude'); finish(done); }); it('should throw an error when including a file that does not exist', function(done) { render( '{% include "missing.njk" %}', {}, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match(/template not found: missing.njk/); } ); finish(done); }); it('should fail silently on missing templates if requested', function(done) { equal('hello world {% include "missing.njk" ignore missing %}', 'hello world '); equal('hello world {% include "missing.njk" ignore missing %}', { name: 'thedude' }, 'hello world '); finish(done); }); /** * This test checks that this issue is resolved: http://stackoverflow.com/questions/21777058/loop-index-in-included-nunjucks-file */ it('should have access to "loop" inside an include', function(done) { equal('{% for item in [1,2,3] %}{% include "include-in-loop.njk" %}{% endfor %}', '1,0,true\n2,1,false\n3,2,false\n'); equal('{% for k,v in items %}{% include "include-in-loop.njk" %}{% endfor %}', { items: { a: 'A', b: 'B' } }, '1,0,true\n2,1,false\n'); finish(done); }); it('should maintain nested scopes', function(done) { equal( '{% for i in [1,2] %}' + '{% for i in [3,4] %}{{ i }}{% endfor %}' + '{{ i }}{% endfor %}', '341342'); finish(done); }); it('should allow blocks in for loops', function(done) { equal( '{% extends "base2.njk" %}' + '{% block item %}hello{{ item }}{% endblock %}', 'hello1hello2'); finish(done); }); it('should make includes inherit scope', function(done) { equal( '{% for item in [1,2] %}' + '{% include "item.njk" %}' + '{% endfor %}', 'showing 1showing 2'); finish(done); }); it('should compile a set block', function(done) { equal('{% set username = "foo" %}{{ username }}', { username: 'james' }, 'foo'); equal('{% set x, y = "foo" %}{{ x }}{{ y }}', 'foofoo'); equal('{% set x = 1 + 2 %}{{ x }}', '3'); equal('{% for i in [1] %}{% set foo=1 %}{% endfor %}{{ foo }}', { foo: 2 }, '2'); equal('{% include "set.njk" %}{{ foo }}', { foo: 'bar' }, 'bar'); equal('{% set username = username + "pasta" %}{{ username }}', { username: 'basta' }, 'bastapasta'); // `set` should only set within its current scope equal( '{% for i in [1] %}{% set val=5 %}{% endfor %}' + '{{ val }}', ''); equal( '{% for i in [1,2,3] %}' + '{% if not val %}{% set val=5 %}{% endif %}' + '{% set val=val+1 %}{{ val }}' + '{% endfor %}' + 'afterwards: {{ val }}', '678afterwards: '); // however, like Python, if a variable has been set in an // above scope, any other set should correctly resolve to // that frame equal( '{% set val=1 %}' + '{% for i in [1] %}{% set val=5 %}{% endfor %}' + '{{ val }}', '5'); equal( '{% set val=5 %}' + '{% for i in [1,2,3] %}' + '{% set val=val+1 %}{{ val }}' + '{% endfor %}' + 'afterwards: {{ val }}', '678afterwards: 8'); finish(done); }); it('should compile set with frame references', function(done) { equal('{% set username = user.name %}{{ username }}', { user: { name: 'james' } }, 'james'); finish(done); }); it('should compile set assignments of the same variable', function(done) { equal( '{% set x = "hello" %}' + '{% if false %}{% set x = "world" %}{% endif %}' + '{{ x }}', 'hello'); equal( '{% set x = "blue" %}' + '{% if true %}{% set x = "green" %}{% endif %}' + '{{ x }}', 'green'); finish(done); }); it('should compile block-set', function(done) { equal( '{% set block_content %}{% endset %}' + '{{ block_content }}', '' ); /** * Capture blocks inside macros were printing to the main buffer instead of * the temporary one, see https://github.com/mozilla/nunjucks/issues/914. **/ equal( '{%- macro foo(bar) -%}' + '{%- set test -%}foo{%- endset -%}' + '{{ bar }}{{ test }}' + '{%- endmacro -%}' + '{{ foo("bar") }}', 'barfoo' ); equal( '{% set block_content %}test string{% endset %}' + '{{ block_content }}', 'test string' ); equal( '{% set block_content %}' + '{% for item in [1, 2, 3] %}' + '{% include "item.njk" %} ' + '{% endfor %}' + '{% endset %}' + '{{ block_content }}', 'showing 1 showing 2 showing 3 ' ); equal( '{% set block_content %}' + '{% set inner_block_content %}' + '{% for i in [1, 2, 3] %}' + 'item {{ i }} ' + '{% endfor %}' + '{% endset %}' + '{% for i in [1, 2, 3] %}' + 'inner {{i}}: "{{ inner_block_content }}" ' + '{% endfor %}' + '{% endset %}' + '{{ block_content | safe }}', 'inner 1: "item 1 item 2 item 3 " ' + 'inner 2: "item 1 item 2 item 3 " ' + 'inner 3: "item 1 item 2 item 3 " ' ); equal( '{% set x,y,z %}' + 'cool' + '{% endset %}' + '{{ x }} {{ y }} {{ z }}', 'cool cool cool' ); finish(done); }); it('should compile block-set wrapping an inherited block', function(done) { equal( '{% extends "base-set-wraps-block.njk" %}' + '{% block somevar %}foo{% endblock %}', 'foo\n' ); finish(done); }); it('should throw errors', function(done) { render('{% from "import.njk" import boozle %}', {}, { noThrow: true }, function(err) { expect(err).to.match(/cannot import 'boozle'/); }); finish(done); }); it('should allow custom tag compilation', function(done) { function TestExtension() { this.tags = ['test']; this.parse = function(parser, nodes) { var content; var tag; parser.advanceAfterBlockEnd(); content = parser.parseUntilBlocks('endtest'); tag = new nodes.CallExtension(this, 'run', null, [content]); parser.advanceAfterBlockEnd(); return tag; }; this.run = function(context, content) { // Reverse the string return content().split('').reverse().join(''); }; } equal('{% test %}123456789{% endtest %}', null, { extensions: { TestExtension: new TestExtension() } }, '987654321'); finish(done); }); it('should allow custom tag compilation without content', function(done) { function TestExtension() { // jshint validthis: true this.tags = ['test']; this.parse = function(parser, nodes) { var tok = parser.nextToken(); var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); return new nodes.CallExtension(this, 'run', args, null); }; this.run = function(context, arg1) { // Reverse the string return arg1.split('').reverse().join(''); }; } equal('{% test "123456" %}', null, { extensions: { TestExtension: new TestExtension() } }, '654321'); finish(done); }); it('should allow complicated custom tag compilation', function(done) { function TestExtension() { // jshint validthis: true this.tags = ['test']; /* normally this is automatically done by Environment */ this._name = TestExtension; this.parse = function(parser, nodes, lexer) { var body; var intermediate = null; parser.advanceAfterBlockEnd(); body = parser.parseUntilBlocks('intermediate', 'endtest'); if (parser.skipSymbol('intermediate')) { parser.skip(lexer.TOKEN_BLOCK_END); intermediate = parser.parseUntilBlocks('endtest'); } parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, 'run', null, [body, intermediate]); }; this.run = function(context, body, intermediate) { var output = body().split('').join(','); if (intermediate) { // Reverse the string. output += intermediate().split('').reverse().join(''); } return output; }; } equal('{% test %}abcdefg{% endtest %}', null, { extensions: { TestExtension: new TestExtension() } }, 'a,b,c,d,e,f,g'); equal('{% test %}abcdefg{% intermediate %}second half{% endtest %}', null, { extensions: { TestExtension: new TestExtension() } }, 'a,b,c,d,e,f,gflah dnoces'); finish(done); }); it('should allow custom tag with args compilation', function(done) { var opts; function TestExtension() { // jshint validthis: true this.tags = ['test']; /* normally this is automatically done by Environment */ this._name = TestExtension; this.parse = function(parser, nodes) { var body; var args; var tok = parser.nextToken(); // passing true makes it tolerate when no args exist args = parser.parseSignature(true); parser.advanceAfterBlockEnd(tok.value); body = parser.parseUntilBlocks('endtest'); parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, 'run', args, [body]); }; this.run = function(context, prefix, kwargs, body) { var output; if (typeof prefix === 'function') { body = prefix; prefix = ''; kwargs = {}; } else if (typeof kwargs === 'function') { body = kwargs; kwargs = {}; } output = prefix + body().split('').reverse().join(''); if (kwargs.cutoff) { output = output.slice(0, kwargs.cutoff); } return output; }; } opts = { extensions: { TestExtension: new TestExtension() } }; equal( '{% test %}foobar{% endtest %}', null, opts, 'raboof'); equal( '{% test("biz") %}foobar{% endtest %}', null, opts, 'bizraboof'); equal( '{% test("biz", cutoff=5) %}foobar{% endtest %}', null, opts, 'bizra'); finish(done); }); it('should autoescape by default', function(done) { equal('{{ foo }}', { foo: '"\'<>&' }, '"'<>&'); finish(done); }); it('should autoescape if autoescape is on', function(done) { equal( '{{ foo }}', { foo: '"\'<>&' }, { autoescape: true }, '"'<>&'); equal('{{ foo|reverse }}', { foo: '"\'<>&' }, { autoescape: true }, '&><'"'); equal( '{{ foo|reverse|safe }}', { foo: '"\'<>&' }, { autoescape: true }, '&><\'"'); equal( '{{ foo }}', { foo: null }, { autoescape: true }, ''); equal( '{{ foo }}', { foo: ['

foo

'] }, { autoescape: true }, '<p>foo</p>'); equal( '{{ foo }}', { foo: { toString: function() { return '

foo

'; } } }, { autoescape: true }, '<p>foo</p>'); equal('{{ foo | safe }}', { foo: null }, { autoescape: true }, ''); equal( '{{ foo | safe }}', { foo: '

foo

' }, { autoescape: true }, '

foo

'); equal( '{{ foo | safe }}', { foo: ['

foo

'] }, { autoescape: true }, '

foo

'); equal( '{{ foo | safe }}', { foo: { toString: function() { return '

foo

'; } } }, { autoescape: true }, '

foo

'); finish(done); }); it('should not autoescape safe strings', function(done) { equal( '{{ foo|safe }}', { foo: '"\'<>&' }, { autoescape: true }, '"\'<>&'); finish(done); }); it('should not autoescape macros', function(done) { render( '{% macro foo(x, y) %}{{ x }} and {{ y }}{% endmacro %}' + '{{ foo("<>&", "<>") }}', null, { autoescape: true }, function(err, res) { expect(res).to.be('<>& and <>'); } ); render( '{% macro foo(x, y) %}{{ x|safe }} and {{ y }}{% endmacro %}' + '{{ foo("<>&", "<>") }}', null, { autoescape: true }, function(err, res) { expect(res).to.be('<>& and <>'); } ); finish(done); }); it('should not autoescape super()', function(done) { render( '{% extends "base3.njk" %}' + '{% block block1 %}{{ super() }}{% endblock %}', null, { autoescape: true }, function(err, res) { expect(res).to.be('Foo'); } ); finish(done); }); it('should not autoescape when extension set false', function(done) { function TestExtension() { // jshint validthis: true this.tags = ['test']; this.autoescape = false; this.parse = function(parser, nodes) { var tok = parser.nextToken(); var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); return new nodes.CallExtension(this, 'run', args, null); }; this.run = function() { // Reverse the string return 'Foo'; }; } render( '{% test "123456" %}', null, { extensions: { TestExtension: new TestExtension() }, autoescape: true }, function(err, res) { expect(res).to.be('Foo'); } ); finish(done); }); it('should pass context as this to filters', function(done) { render( '{{ foo | hallo }}', { foo: 1, bar: 2 }, { filters: { hallo: function(foo) { return foo + this.lookup('bar'); } } }, function(err, res) { expect(res).to.be('3'); } ); finish(done); }); it('should render regexs', function(done) { equal('{{ r/name [0-9] \\// }}', '/name [0-9] \\//'); equal('{{ r/x/gi }}', '/x/gi'); finish(done); }); it('should throw an error when {% call %} is passed an object that is not a function', function(done) { render( '{% call foo() %}{% endcall %}', {foo: 'bar'}, {noThrow: true}, function(err, res) { expect(res).to.be(undefined); expect(err).to.match(/Unable to call `\w+`, which is not a function/); }); finish(done); }); it('should throw an error when including a file that calls an undefined macro', function(done) { render( '{% include "undefined-macro.njk" %}', {}, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match(/Unable to call `\w+`, which is undefined or falsey/); } ); finish(done); }); it('should throw an error when including a file that calls an undefined macro even inside {% if %} tag', function(done) { render( '{% if true %}{% include "undefined-macro.njk" %}{% endif %}', {}, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match(/Unable to call `\w+`, which is undefined or falsey/); } ); finish(done); }); it('should throw an error when including a file that imports macro that calls an undefined macro', function(done) { render( '{% include "import-macro-call-undefined-macro.njk" %}', { list: [1, 2, 3] }, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match(/Unable to call `\w+`, which is undefined or falsey/); } ); finish(done); }); it('should control whitespaces correctly', function(done) { equal( '{% if true -%}{{"hello"}} {{"world"}}{% endif %}', 'hello world'); equal( '{% if true -%}{% if true %} {{"hello"}} {{"world"}}' + '{% endif %}{% endif %}', ' hello world'); equal( '{% if true -%}{# comment #} {{"hello"}}{% endif %}', ' hello'); finish(done); }); it('should control expression whitespaces correctly', function(done) { equal( 'Well, {{- \' hello, \' -}} my friend', 'Well, hello, my friend' ); equal(' {{ 2 + 2 }} ', ' 4 '); equal(' {{-2 + 2 }} ', '4 '); equal(' {{ -2 + 2 }} ', ' 0 '); equal(' {{ 2 + 2 -}} ', ' 4'); finish(done); }); it('should get right value when macro parameter conflict with global macro name', function(done) { render( '{# macro1 and macro2 definition #}' + '{% macro macro1() %}' + '{% endmacro %}' + '' + '{% macro macro2(macro1="default") %}' + '{{macro1}}' + '{% endmacro %}' + '' + '{# calling macro2 #}' + '{{macro2("this should be outputted") }}', {}, {}, function(err, res) { expect(res.trim()).to.eql('this should be outputted'); }); finish(done); }); it('should get right value when macro include macro', function(done) { render( '{# macro1 and macro2 definition #}' + '{% macro macro1() %} foo' + '{% endmacro %}' + '' + '{% macro macro2(text="default") %}' + '{{macro1()}}' + '{% endmacro %}' + '' + '{# calling macro2 #}' + '{{macro2("this should not be outputted") }}', {}, {}, function(err, res) { expect(res.trim()).to.eql('foo'); }); finish(done); }); it('should allow access to outer scope in call blocks', function(done) { render( '{% macro inside() %}' + '{{ caller() }}' + '{% endmacro %}' + '{% macro outside(var) %}' + '{{ var }}\n' + '{% call inside() %}' + '{{ var }}' + '{% endcall %}' + '{% endmacro %}' + '{{ outside("foobar") }}', {}, {}, function(err, res) { expect(res.trim()).to.eql('foobar\nfoobar'); }); finish(done); }); it('should not leak scope from call blocks to parent', function(done) { render( '{% set var = "expected" %}' + '{% macro inside() %}' + '{% set var = "incorrect-value" %}' + '{{ caller() }}' + '{% endmacro %}' + '{% macro outside() %}' + '{% call inside() %}' + '{% endcall %}' + '{% endmacro %}' + '{{ outside() }}' + '{{ var }}', {}, {}, function(err, res) { expect(res.trim()).to.eql('expected'); }); finish(done); }); if (!isSlim) { it('should import template objects', function(done) { var tmpl = new Template('{% macro foo() %}Inside a macro{% endmacro %}' + '{% set bar = "BAZ" %}'); equal( '{% import tmpl as imp %}' + '{{ imp.foo() }} {{ imp.bar }}', { tmpl: tmpl }, 'Inside a macro BAZ'); equal( '{% from tmpl import foo as baz, bar %}' + '{{ bar }} {{ baz() }}', { tmpl: tmpl }, 'BAZ Inside a macro'); finish(done); }); it('should inherit template objects', function(done) { var tmpl = new Template('Foo{% block block1 %}Bar{% endblock %}' + '{% block block2 %}Baz{% endblock %}Whizzle'); equal('hola {% extends tmpl %} fizzle mumble', { tmpl: tmpl }, 'FooBarBazWhizzle'); equal( '{% extends tmpl %}' + '{% block block1 %}BAR{% endblock %}' + '{% block block2 %}BAZ{% endblock %}', { tmpl: tmpl }, 'FooBARBAZWhizzle'); finish(done); }); it('should include template objects', function(done) { var tmpl = new Template('FooInclude {{ name }}'); equal('hello world {% include tmpl %}', { name: 'thedude', tmpl: tmpl }, 'hello world FooInclude thedude'); finish(done); }); it('should throw an error when invalid expression whitespaces are used', function(done) { render( ' {{ 2 + 2- }}', {}, { noThrow: true }, function(err, res) { expect(res).to.be(undefined); expect(err).to.match(/unexpected token: }}/); } ); finish(done); }); } }); describe('the filter tag', function() { it('should apply the title filter to the body', function(done) { equal('{% filter title %}may the force be with you{% endfilter %}', 'May The Force Be With You'); finish(done); }); it('should apply the replace filter to the body', function(done) { equal('{% filter replace("force", "forth") %}may the force be with you{% endfilter %}', 'may the forth be with you'); finish(done); }); it('should work with variables in the body', function(done) { equal('{% set foo = "force" %}{% filter replace("force", "forth") %}may the {{ foo }} be with you{% endfilter %}', 'may the forth be with you'); finish(done); }); it('should work with blocks in the body', function(done) { equal( '{% extends "filter-block.html" %}' + '{% block block1 %}force{% endblock %}', 'may the forth be with you\n'); finish(done); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/core.js000664 000000 000000 00000003615 14012546311 022510 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect, nunjucks, fs, os, path; if (typeof require !== 'undefined') { expect = require('expect.js'); nunjucks = require('../nunjucks/index'); fs = require('fs-extra'); path = require('path'); os = require('os'); } else { expect = window.expect; nunjucks = window.nunjucks; } function rmdir(dirPath) { fs.emptyDirSync(dirPath); fs.rmdirSync(dirPath); } describe('nunjucks.configure', function() { var tempdir; before(function() { if (fs && path && os) { try { tempdir = fs.mkdtempSync(path.join(os.tmpdir(), 'templates')); fs.emptyDirSync(tempdir); } catch (e) { rmdir(tempdir); throw e; } } }); after(function() { nunjucks.reset(); if (typeof tempdir !== 'undefined') { rmdir(tempdir); } }); it('should cache templates by default', function() { if (typeof fs === 'undefined') { this.skip(); return; } nunjucks.configure(tempdir); fs.writeFileSync(tempdir + '/test.html', '{{ name }}', 'utf-8'); expect(nunjucks.render('test.html', {name: 'foo'})).to.be('foo'); fs.writeFileSync(tempdir + '/test.html', '{{ name }}-changed', 'utf-8'); expect(nunjucks.render('test.html', {name: 'foo'})).to.be('foo'); }); it('should not cache templates with {noCache: true}', function() { if (typeof fs === 'undefined') { this.skip(); return; } nunjucks.configure(tempdir, {noCache: true}); fs.writeFileSync(tempdir + '/test.html', '{{ name }}', 'utf-8'); expect(nunjucks.render('test.html', {name: 'foo'})).to.be('foo'); fs.writeFileSync(tempdir + '/test.html', '{{ name }}-changed', 'utf-8'); expect(nunjucks.render('test.html', {name: 'foo'})).to.be('foo-changed'); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/express.js000664 000000 000000 00000002113 14012546311 023241 0ustar00rootroot000000 000000 'use strict'; var path = require('path'); var express = require('express'); var expect = require('expect.js'); var request = require('supertest'); var nunjucks = require('../nunjucks/index'); var VIEWS = path.join(__dirname, '../samples/express/views'); describe('express', function() { var app; var env; beforeEach(function() { app = express(); env = new nunjucks.Environment(new nunjucks.FileSystemLoader(VIEWS)); env.express(app); }); it('should have reference to nunjucks env', function() { expect(app.settings.nunjucksEnv).to.be(env); }); it('should render a view with extension', function(done) { app.get('/', function(req, res) { res.render('about.html'); }); request(app) .get('/') .expect(/This is just the about page/) .end(done); }); it('should render a view without extension', function(done) { app.get('/', function(req, res) { res.render('about'); }); app.set('view engine', 'html'); request(app) .get('/') .expect(/This is just the about page/) .end(done); }); }); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/filters.js000664 000000 000000 00000077712 14012546311 023241 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect; var util; var lib; var r; var render; var equal; var finish; if (typeof require !== 'undefined') { expect = require('expect.js'); util = require('./util'); lib = require('../nunjucks/src/lib'); r = require('../nunjucks/src/runtime'); } else { expect = window.expect; util = window.util; lib = nunjucks.lib; r = nunjucks.runtime; } render = util.render; equal = util.equal; finish = util.finish; describe('filter', function() { it('abs', function(done) { equal('{{ -3|abs }}', '3'); equal('{{ -3.456|abs }}', '3.456'); finish(done); }); it('batch', function(done) { equal( [ '{% for a in [1,2,3,4,5,6]|batch(2) %}', '-{% for b in a %}', '{{ b }}', '{% endfor %}-', '{% endfor %}'].join(''), '-12--34--56-'); finish(done); }); it('capitalize', function(done) { equal('{{ "foo" | capitalize }}', 'Foo'); equal('{{ str | capitalize }}', { str: r.markSafe('foo') }, 'Foo'); equal('{{ undefined | capitalize }}', ''); equal('{{ null | capitalize }}', ''); equal('{{ nothing | capitalize }}', ''); finish(done); }); it('center', function(done) { equal('{{ "fooo" | center }}', lib.repeat(' ', 38) + 'fooo' + lib.repeat(' ', 38)); equal('{{ str | center }}', {str: r.markSafe('fooo')}, lib.repeat(' ', 38) + 'fooo' + lib.repeat(' ', 38)); equal('{{ undefined | center }}', lib.repeat(' ', 40) + '' + lib.repeat(' ', 40)); equal('{{ null | center }}', lib.repeat(' ', 40) + '' + lib.repeat(' ', 40)); equal('{{ nothing | center }}', lib.repeat(' ', 40) + '' + lib.repeat(' ', 40)); equal('{{ "foo" | center }}', lib.repeat(' ', 38) + 'foo' + lib.repeat(' ', 39)); finish(done); }); it('default', function(done) { equal('{{ undefined | default("foo") }}', 'foo'); equal('{{ bar | default("foo") }}', { bar: null }, ''); equal('{{ false | default("foo") }}', 'false'); equal('{{ false | default("foo", true) }}', 'foo'); equal('{{ bar | default("foo") }}', 'foo'); equal('{{ "bar" | default("foo") }}', 'bar'); finish(done); }); it('dump', function() { equal('{{ [\'a\', 1, {b: true}] | dump }}', '["a",1,{"b":true}]'); equal('{{ [\'a\', 1, {b: true}] | dump(2) }}', '[\n "a",\n 1,\n {\n "b": true\n }\n]'); equal('{{ [\'a\', 1, {b: true}] | dump(4) }}', '[\n "a",\n 1,\n {\n "b": true\n }\n]'); equal('{{ [\'a\', 1, {b: true}] | dump(\'\t\') }}', '[\n\t"a",\n\t1,\n\t{\n\t\t"b": true\n\t}\n]'); }); it('escape', function() { equal( '{{ "" | escape }}', {}, { autoescape: false }, '<html>'); }); it('escape skip safe', function() { equal('{{ "" | safe | escape }}', {}, { autoescape: false }, ''); }); it('should not double escape strings', function() { equal('{{ "" | escape | escape }}', {}, { autoescape: false }, '<html>'); }); it('should not double escape with autoescape on', function() { equal('{% set val = "" | escape %}{{ val }}', {}, { autoescape: true }, '<html>'); }); it('should work with non-string values', function() { equal( '{{ foo | escape }}', { foo: [''] }, { autoescape: false }, '<html>'); equal( '{{ foo | escape }}', { foo: { toString: function() { return ''; } } }, { autoescape: false }, '<html>'); equal('{{ foo | escape }}', { foo: null }, { autoescape: false }, ''); }); it('should not escape safe strings with autoescape on', function() { equal( '{{ "" | safe | escape }}', {}, { autoescape: true }, ''); equal( '{% set val = "" | safe | e %}{{ val }}', {}, { autoescape: true }, ''); }); it('should keep strings escaped after they have been escaped', function() { equal( '{% set val = "" | e | safe %}{{ val }}', {}, { autoescape: false }, '<html>'); }); it('dictsort', function(done) { // no real foolproof way to test that a js obj has been transformed // from unsorted -> sorted, as its enumeration ordering is undefined // and might fluke being sorted originally .. lets just init with some jumbled // keys // no params - should be case insensitive, by key equal( '{% for item in items | dictsort %}' + '{{ item[0] }}{% endfor %}', { items: { e: 1, d: 2, c: 3, a: 4, f: 5, b: 6 } }, 'abcdef'); // case sensitive = true equal( '{% for item in items | dictsort(true) %}{{ item[0] }},{% endfor %}', { items: { ABC: 6, ABc: 5, Abc: 1, abc: 2 } }, 'ABC,ABc,Abc,abc,'); // use values for sort equal( '{% for item in items | dictsort(false, "value") %}{{ item[0] }}{% endfor %}', { items: { a: 6, b: 5, c: 1, d: 2 } }, 'cdba'); finish(done); }); it('first', function(done) { equal('{{ [1,2,3] | first }}', '1'); finish(done); }); it('float', function() { equal('{{ "3.5" | float }}', '3.5'); equal('{{ "0" | float }}', '0'); }); it('forceescape', function(done) { equal('{{ str | forceescape }}', { str: r.markSafe('')}, '<html>'); equal('{{ "" | safe | forceescape }}', '<html>'); finish(done); }); it('int', function() { equal('{{ "3.5" | int }}', '3'); equal('{{ "0" | int }}', '0'); equal('{{ "foobar" | int("42") }}', '42'); equal('{{ "0x4d32" | int(base=16) }}', '19762'); equal('{{ "011" | int(base=8) }}', '9'); }); it('int (default value)', function() { equal('{{ "bob" | int("cat") }}', 'cat'); }); it('float (default value)', function() { equal('{{ "bob" | float("cat") }}', 'cat'); }); it('groupby', function(done) { const namesContext = { items: [{ name: 'james', type: 'green' }, { name: 'john', type: 'blue' }, { name: 'jim', type: 'blue' }, { name: 'jessie', type: 'green' }] }; equal( '{% for type, items in items | groupby("type") %}' + ':{{ type }}:' + '{% for item in items %}' + '{{ item.name }}' + '{% endfor %}' + '{% endfor %}', namesContext, ':green:jamesjessie:blue:johnjim'); equal( '{% for type, items in items | groupby("type") %}' + ':{{ type }}:' + '{% for item in items %}' + '{{ item.name }}' + '{% endfor %}' + '{% endfor %}', { items: [{ name: 'james', type: 'green' }, { name: 'john', type: 'blue' }, { name: 'jim', type: 'blue' }, { name: 'jessie', color: 'green' }] }, ':green:james:blue:johnjim:undefined:jessie'); equal( '{% for year, posts in posts | groupby("date.year") %}' + ':{{ year }}:' + '{% for post in posts %}' + '{{ post.title }}' + '{% endfor %}' + '{% endfor %}', { posts: [ { date: { year: 2019 }, title: 'Post 1' }, { date: { year: 2018 }, title: 'Post 2' }, { date: { year: 2019 }, title: 'Post 3' } ] }, ':2018:Post 2:2019:Post 1Post 3'); equal( '{% for year, posts in posts | groupby("date.year") %}' + ':{{ year }}:' + '{% for post in posts %}' + '{{ post.title }}' + '{% endfor %}' + '{% endfor %}', { posts: [ { date: { year: 2019 }, title: 'Post 1' }, { date: { year: 2018 }, title: 'Post 2' }, { meta: { month: 2 }, title: 'Post 3' } ] }, ':2018:Post 2:2019:Post 1:undefined:Post 3'); equal( '{% for type, items in items | groupby({}) %}' + ':{{ type }}:' + '{% for item in items %}' + '{{ item.name }}' + '{% endfor %}' + '{% endfor %}', namesContext, ':undefined:jamesjohnjimjessie' ); const undefinedTemplate = ( '{% for type, items in items | groupby("a.b.c") %}' + ':{{ type }}:' + '{% for item in items %}' + '{{ item.name }}' + '{% endfor %}' + '{% endfor %}' ); equal( undefinedTemplate, namesContext, ':undefined:jamesjohnjimjessie' ); expect(function() { render( undefinedTemplate, namesContext, { throwOnUndefined: true } ); }).to.throwError(/groupby: attribute "a\.b\.c" resolved to undefined/); finish(done); }); it('indent', function(done) { equal('{{ "one\ntwo\nthree" | indent }}', 'one\n two\n three'); equal('{{ "one\ntwo\nthree" | indent(2) }}', 'one\n two\n three'); equal('{{ "one\ntwo\nthree" | indent(2, true) }}', ' one\n two\n three'); equal('{{ str | indent }}', { str: r.markSafe('one\ntwo\nthree') }, 'one\n two\n three'); equal('{{ "" | indent }}', ''); equal('{{ undefined | indent }}', ''); equal('{{ undefined | indent(2) }}', ''); equal('{{ undefined | indent(2, true) }}', ''); equal('{{ null | indent }}', ''); equal('{{ null | indent(2) }}', ''); equal('{{ null | indent(2, true) }}', ''); equal('{{ nothing | indent }}', ''); equal('{{ nothing | indent(2) }}', ''); equal('{{ nothing | indent(2, true) }}', ''); finish(done); }); it('join', function(done) { equal('{{ items | join }}', { items: [1, 2, 3] }, '123'); equal('{{ items | join(",") }}', { items: ['foo', 'bar', 'bear'] }, 'foo,bar,bear'); equal('{{ items | join(",", "name") }}', { items: [{ name: 'foo' }, { name: 'bar' }, { name: 'bear' }] }, 'foo,bar,bear'); finish(done); }); it('last', function(done) { equal('{{ [1,2,3] | last }}', '3'); finish(done); }); describe('the length filter', function suite() { it('should return length of a list literal', function test() { equal('{{ [1,2,3] | length }}', '3'); }); it('should output 0 for a missing context variable', function test() { equal('{{ blah|length }}', '0'); }); it('should output string length for string variables', function test() { equal('{{ str | length }}', { str: 'blah' }, '4'); }); it('should output string length for a SafeString variable', function test() { equal('{{ str | length }}', { str: r.markSafe('') }, '6'); }); it('should output the correct length of a string created with new String()', function test() { equal('{{ str | length }}', { str: new String('blah') // eslint-disable-line no-new-wrappers }, '4'); }); it('should output 0 for a literal "undefined"', function test() { equal('{{ undefined | length }}', '0'); }); it('should output 0 for a literal "null"', function test() { equal('{{ null | length }}', '0'); }); it('should output 0 for an Object with no properties', function test() { equal('{{ obj | length }}', { obj: {} }, '0'); }); it('should output 1 for an Object with 1 property', function test() { equal('{{ obj | length }}', { obj: { key: 'value' } }, '1'); }); it('should output the number of properties for a plain Object, not the value of its length property', function test() { equal('{{ obj | length }}', { obj: { key: 'value', length: 5 } }, '2'); }); it('should output the length of an array', function test() { equal('{{ arr | length }}', { arr: [0, 1] }, '2'); }); it('should output the full length of a sparse array', function test() { equal('{{ arr | length }}', { arr: [0,, 2] // eslint-disable-line }, '3'); }); it('should output the length of an array created with "new Array"', function test() { equal('{{ arr | length }}', { arr: new Array(0, 1) // eslint-disable-line no-array-constructor }, '2'); }); it('should output the length of an array created with "new Array" with user-defined properties', function test() { var arr = new Array(0, 1); // eslint-disable-line no-array-constructor arr.key = 'value'; equal('{{ arr | length }}', { arr: arr }, '2'); }); it('should output the length of a Map', function test() { /* global Map */ var map; if (typeof Map === 'undefined') { this.skip(); } else { map = new Map([['key1', 'value1'], ['key2', 'value2']]); map.set('key3', 'value3'); equal('{{ map | length }}', { map: map }, '3'); } }); it('should output the length of a Set', function test() { /* global Set */ var set; if (typeof Set === 'undefined') { this.skip(); } else { set = new Set(['value1']); set.add('value2'); equal('{{ set | length }}', { set: set }, '2'); } }); }); it('list', function(done) { var person = { name: 'Joe', age: 83 }; equal('{% for i in "foobar" | list %}{{ i }},{% endfor %}', 'f,o,o,b,a,r,'); equal('{% for pair in person | list %}{{ pair.key }}: {{ pair.value }} - {% endfor %}', { person: person }, 'name: Joe - age: 83 - '); equal('{% for i in [1, 2] | list %}{{ i }}{% endfor %}', '12'); finish(done); }); it('lower', function(done) { equal('{{ "fOObAr" | lower }}', 'foobar'); equal('{{ str | lower }}', { str: r.markSafe('fOObAr') }, 'foobar'); equal('{{ null | lower }}', ''); equal('{{ undefined | lower }}', ''); equal('{{ nothing | lower }}', ''); finish(done); }); it('nl2br', function(done) { equal('{{ null | nl2br }}', ''); equal('{{ undefined | nl2br }}', ''); equal('{{ nothing | nl2br }}', ''); equal('{{ str | nl2br }}', { str: r.markSafe('foo\r\nbar') }, 'foo
\nbar'); equal('{{ str | nl2br }}', { str: r.markSafe('foo\nbar') }, 'foo
\nbar'); equal('{{ str | nl2br }}', { str: r.markSafe('foo\n\nbar') }, 'foo
\n
\nbar'); equal('{{ "foo\nbar" | nl2br }}', 'foo<br />\nbar'); finish(done); }); it('random', function(done) { var i; for (i = 0; i < 100; i++) { render('{{ [1,2,3,4,5,6,7,8,9] | random }}', function(err, res) { var val = parseInt(res, 10); expect(val).to.be.within(1, 9); }); } finish(done); }); it('reject', function(done) { var context = { numbers: [0, 1, 2, 3, 4, 5] }; equal('{{ numbers | reject("odd") | join }}', context, '024'); equal('{{ numbers | reject("even") | join }}', context, '135'); equal('{{ numbers | reject("divisibleby", 3) | join }}', context, '1245'); equal('{{ numbers | reject() | join }}', context, '0'); finish(done); }); it('rejectattr', function(done) { var foods = [{ tasty: true }, { tasty: false }, { tasty: true }]; equal('{{ foods | rejectattr("tasty") | length }}', { foods: foods }, '1'); finish(done); }); it('select', function(done) { var context = { numbers: [0, 1, 2, 3, 4, 5] }; equal('{{ numbers | select("odd") | join }}', context, '135'); equal('{{ numbers | select("even") | join }}', context, '024'); equal('{{ numbers | select("divisibleby", 3) | join }}', context, '03'); equal('{{ numbers | select() | join }}', context, '12345'); finish(done); }); it('selectattr', function(done) { var foods = [{ tasty: true }, { tasty: false }, { tasty: true }]; equal('{{ foods | selectattr("tasty") | length }}', { foods: foods }, '2'); finish(done); }); it('replace', function(done) { equal('{{ 123456 | replace("4", ".") }}', '123.56'); equal('{{ 123456 | replace("4", ".") }}', '123.56'); equal('{{ 12345.6 | replace("4", ".") }}', '123.5.6'); equal('{{ 12345.6 | replace(4, ".") }}', '123.5.6'); equal('{{ 12345.6 | replace("4", "7") }}', '12375.6'); equal('{{ 12345.6 | replace(4, 7) }}', '12375.6'); equal('{{ 123450.6 | replace(0, 7) }}', '123457.6'); equal('{{ "aaabbbccc" | replace("", ".") }}', '.a.a.a.b.b.b.c.c.c.'); equal('{{ "aaabbbccc" | replace(null, ".") }}', 'aaabbbccc'); equal('{{ "aaabbbccc" | replace(undefined, ".") }}', 'aaabbbccc'); equal('{{ "aaabbbccc" | replace({}, ".") }}', 'aaabbbccc'); equal('{{ "aaabbbccc" | replace(true, ".") }}', 'aaabbbccc'); equal('{{ "aaabbbccc" | replace(false, ".") }}', 'aaabbbccc'); equal('{{ "aaabbbccc" | replace(["wrong"], ".") }}', 'aaabbbccc'); equal('{{ "aaabbbccc" | replace("a", "x") }}', 'xxxbbbccc'); equal('{{ "aaabbbccc" | replace("a", "x", 2) }}', 'xxabbbccc'); equal('{{ "aaabbbbbccc" | replace("b", "y", 4) }}', 'aaayyyybccc'); equal('{{ "aaabbbbbccc" | replace("", "") }}', 'aaabbbbbccc'); equal('{{ "aaabbbbbccc" | replace("b", "") }}', 'aaaccc'); equal('{{ "aaabbbbbccc" | replace("b", "", 4) }}', 'aaabccc'); equal('{{ "aaabbbbbccc" | replace("ab", "y", 4) }}', 'aaybbbbccc'); equal('{{ "aaabbbbbccc" | replace("b", "y", 4) }}', 'aaayyyybccc'); equal('{{ "aaabbbbbccc" | replace("d", "y", 4) }}', 'aaabbbbbccc'); equal('{{ "aaabbcccbbb" | replace("b", "y", 4) }}', 'aaayycccyyb'); // Bad initial inputs equal('{{ undefined | replace("b", "y", 4) }}', ''); equal('{{ null | replace("b", "y", 4) }}', ''); equal('{{ {} | replace("b", "y", 4) }}', '[object Object]'); // End up with the object passed out of replace, then toString called on it equal('{{ [] | replace("b", "y", 4) }}', ''); equal('{{ true | replace("rue", "afafasf", 4) }}', 'true'); equal('{{ false | replace("rue", "afafasf", 4) }}', 'false'); // Will result in an infinite loop if unbounded otherwise test will pass equal('{{ "val")" | replace("'); // Regex equal('{{ "aabbbb" | replace(r/ab{2}/, "z") }}', 'azbb'); equal('{{ "aaaAAA" | replace(r/a/i, "z") }}', 'zaaAAA'); equal('{{ "aaaAAA" | replace(r/a/g, "z") }}', 'zzzAAA'); equal('{{ "aaaAAA" | replace(r/a/gi, "z") }}', 'zzzzzz'); equal('{{ str | replace("a", "x") }}', { str: r.markSafe('aaabbbccc') }, 'xxxbbbccc'); finish(done); }); it('reverse', function(done) { equal('{{ "abcdef" | reverse }}', 'fedcba'); equal('{% for i in [1, 2, 3, 4] | reverse %}{{ i }}{% endfor %}', '4321'); finish(done); }); it('round', function(done) { equal('{{ 4.5 | round }}', '5'); equal('{{ 4.5 | round(0, "floor") }}', '4'); equal('{{ 4.12345 | round(4) }}', '4.1235'); equal('{{ 4.12344 | round(4) }}', ('4.1234')); finish(done); }); it('slice', function(done) { var tmpl = '{% for items in arr | slice(3) %}' + '--' + '{% for item in items %}' + '{{ item }}' + '{% endfor %}' + '--' + '{% endfor %}'; equal(tmpl, { arr: [1, 2, 3, 4, 5, 6, 7, 8, 9] }, '--123----456----789--'); equal(tmpl, { arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }, '--1234----567----8910--'); finish(done); }); it('sum', function(done) { equal('{{ items | sum }}', { items: [1, 2, 3] }, '6'); equal('{{ items | sum("value") }}', { items: [{ value: 1 }, { value: 2 }, { value: 3 }] }, '6'); equal('{{ items | sum("value", 10) }}', { items: [ {value: 1}, {value: 2}, {value: 3} ] }, '16'); finish(done); }); it('sort', function(done) { equal('{% for i in [3,5,2,1,4,6] | sort %}{{ i }}{% endfor %}', '123456'); equal('{% for i in ["fOo", "Foo"] | sort %}{{ i }}{% endfor %}', 'fOoFoo'); equal('{% for i in [1,6,3,7] | sort(true) %}{{ i }}{% endfor %}', '7631'); equal('{% for i in ["fOo", "Foo"] | sort(false, true) %}{{ i }}{% endfor %}', 'FoofOo'); equal('{% for item in items | sort(false, false, "name") %}{{ item.name }}{% endfor %}', { items: [ {name: 'james'}, {name: 'fred'}, {name: 'john'} ] }, 'fredjamesjohn'); equal('{% for i in [ {n:3},{n:5},{n:2},{n:1},{n:4},{n:6}] | sort(attribute="n") %}{{ i.n }}{% endfor %}', '123456'); const nestedAttributeSortTemplate = '{% for item in items | sort(attribute="meta.age") %}{{ item.name }}{% endfor %}'; equal( nestedAttributeSortTemplate, { items: [ {name: 'james', meta: {age: 25}}, {name: 'fred', meta: {age: 18}}, {name: 'john', meta: {age: 19}} ] }, 'fredjohnjames' ); expect(function() { render( nestedAttributeSortTemplate, { items: [ {name: 'james', meta: {age: 25}}, {name: 'fred', meta: {age: 18}}, {name: 'john', meta: {title: 'Developer'}} ] }, { throwOnUndefined: true } ); }).to.throwError(/sort: attribute "meta\.age" resolved to undefined/); finish(done); }); it('string', function(done) { equal('{% for i in 1234 | string | list %}{{ i }},{% endfor %}', '1,2,3,4,'); finish(done); }); it('striptags', function(done) { equal('{{ html | striptags }}', { html: 'bar' }, 'bar'); equal('{{ html | striptags }}', { html: '

an \n example link

\n

to a webpage

' + '' }, 'an example link to a webpage'); equal('{{ undefined | striptags }}', ''); equal('{{ null | striptags }}', ''); equal('{{ nothing | striptags }}', ''); equal('{{ html | striptags(true) }}', { html: '
\n row1\nrow2 \n row3\n
\n\n' + ' HEADER \n\n
    \n
  • option 1
  • \n
  • option 2
  • \n
' }, 'row1\nrow2\nrow3\n\nHEADER\n\noption 1\noption 2'); finish(done); }); it('title', function(done) { equal('{{ "foo bar baz" | title }}', 'Foo Bar Baz'); equal('{{ str | title }}', { str: r.markSafe('foo bar baz') }, 'Foo Bar Baz'); equal('{{ undefined | title }}', ''); equal('{{ null | title }}', ''); equal('{{ nothing | title }}', ''); finish(done); }); it('trim', function(done) { equal('{{ " foo " | trim }}', 'foo'); equal('{{ str | trim }}', { str: r.markSafe(' foo ') }, 'foo'); finish(done); }); it('truncate', function(done) { equal('{{ "foo bar" | truncate(3) }}', 'foo...'); equal('{{ "foo bar baz" | truncate(6) }}', 'foo...'); equal('{{ "foo bar baz" | truncate(7) }}', 'foo bar...'); equal('{{ "foo bar baz" | truncate(5, true) }}', 'foo b...'); equal('{{ "foo bar baz" | truncate(6, true, "?") }}', 'foo ba?'); equal('{{ "foo bar" | truncate(3) }}', { str: r.markSafe('foo bar') }, 'foo...'); equal('{{ undefined | truncate(3) }}', ''); equal('{{ undefined | truncate(6) }}', ''); equal('{{ undefined | truncate(7) }}', ''); equal('{{ undefined | truncate(5, true) }}', ''); equal('{{ undefined | truncate(6, true, "?") }}', ''); equal('{{ null | truncate(3) }}', ''); equal('{{ null | truncate(6) }}', ''); equal('{{ null | truncate(7) }}', ''); equal('{{ null | truncate(5, true) }}', ''); equal('{{ null | truncate(6, true, "?") }}', ''); equal('{{ nothing | truncate(3) }}', ''); equal('{{ nothing | truncate(6) }}', ''); equal('{{ nothing | truncate(7) }}', ''); equal('{{ nothing | truncate(5, true) }}', ''); equal('{{ nothing | truncate(6, true, "?") }}', ''); finish(done); }); it('upper', function(done) { equal('{{ "foo" | upper }}', 'FOO'); equal('{{ str | upper }}', { str: r.markSafe('foo') }, 'FOO'); equal('{{ null | upper }}', ''); equal('{{ undefined | upper }}', ''); equal('{{ nothing | upper }}', ''); finish(done); }); it('urlencode', function(done) { equal('{{ "&" | urlencode }}', '%26'); equal('{{ arr | urlencode | safe }}', { arr: [[1, 2], ['&1', '&2']] }, '1=2&%261=%262'); equal('{{ obj | urlencode | safe }}', { obj: { 1: 2, '&1': '&2' } }, '1=2&%261=%262'); finish(done); }); it('urlencode - object without prototype', function(done) { var obj = Object.create(null); obj['1'] = 2; obj['&1'] = '&2'; equal('{{ obj | urlencode | safe }}', { obj: obj }, '1=2&%261=%262'); finish(done); }); it('urlize', function(done) { // from jinja test suite: // https://github.com/mitsuhiko/jinja2/blob/8db47916de0e888dd8664b2511e220ab5ecf5c15/jinja2/testsuite/filters.py#L236-L239 equal('{{ "foo http://www.example.com/ bar" | urlize | safe }}', 'foo ' + 'http://www.example.com/ bar'); // additional tests equal('{{ "" | urlize }}', ''); equal('{{ "foo" | urlize }}', 'foo'); // http equal('{{ "http://jinja.pocoo.org/docs/templates/" | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); // https equal('{{ "https://jinja.pocoo.org/docs/templates/" | urlize | safe }}', 'https://jinja.pocoo.org/docs/templates/'); // www without protocol equal('{{ "www.pocoo.org/docs/templates/" | urlize | safe }}', 'www.pocoo.org/docs/templates/'); // .org, .net, .com without protocol or www equal('{{ "pocoo.org/docs/templates/" | urlize | safe }}', 'pocoo.org/docs/templates/'); equal('{{ "pocoo.net/docs/templates/" | urlize | safe }}', 'pocoo.net/docs/templates/'); equal('{{ "pocoo.com/docs/templates/" | urlize | safe }}', 'pocoo.com/docs/templates/'); equal('{{ "pocoo.com:80" | urlize | safe }}', 'pocoo.com:80'); equal('{{ "pocoo.com" | urlize | safe }}', 'pocoo.com'); equal('{{ "pocoo.commune" | urlize | safe }}', 'pocoo.commune'); // truncate the printed URL equal('{{ "http://jinja.pocoo.org/docs/templates/" | urlize(12, true) | safe }}', 'http://jinja'); // punctuation on the beginning of line. equal('{{ "(http://jinja.pocoo.org/docs/templates/" | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); equal('{{ "http://jinja.pocoo.org/docs/templates/'); equal('{{ "<http://jinja.pocoo.org/docs/templates/" | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); // punctuation on the end of line equal('{{ "http://jinja.pocoo.org/docs/templates/," | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); equal('{{ "http://jinja.pocoo.org/docs/templates/." | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); equal('{{ "http://jinja.pocoo.org/docs/templates/)" | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); equal('{{ "http://jinja.pocoo.org/docs/templates/\n" | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/\n'); equal('{{ "http://jinja.pocoo.org/docs/templates/>" | urlize | safe }}', 'http://jinja.pocoo.org/docs/templates/'); // http url with username equal('{{ "http://testuser@testuser.com" | urlize | safe }}', 'http://testuser@testuser.com'); // email addresses equal('{{ "testuser@testuser.com" | urlize | safe }}', 'testuser@testuser.com'); // periods in the text equal('{{ "foo." | urlize }}', 'foo.'); equal('{{ "foo.foo" | urlize }}', 'foo.foo'); // markup in the text equal('{{ "what up" | urlize | safe }}', 'what up'); // breaklines and tabs in the text equal('{{ "what\nup" | urlize | safe }}', 'what\nup'); equal('{{ "what\tup" | urlize | safe }}', 'what\tup'); finish(done); }); it('wordcount', function(done) { equal('{{ "foo bar baz" | wordcount }}', '3'); equal( '{{ str | wordcount }}', { str: r.markSafe('foo bar baz') }, '3'); equal('{{ null | wordcount }}', ''); equal('{{ undefined | wordcount }}', ''); equal('{{ nothing | wordcount }}', ''); finish(done); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/globals.js000664 000000 000000 00000011277 14012546311 023206 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect; var util; var Environment; var equal; var render; var finish; if (typeof require !== 'undefined') { expect = require('expect.js'); util = require('./util'); Environment = require('../nunjucks/src/environment').Environment; } else { expect = window.expect; util = window.util; Environment = nunjucks.Environment; } equal = util.equal; render = util.render; finish = util.finish; describe('global', function() { it('should have range', function(done) { equal('{% for i in range(0, 10) %}{{ i }}{% endfor %}', '0123456789'); equal('{% for i in range(10) %}{{ i }}{% endfor %}', '0123456789'); equal('{% for i in range(5, 10) %}{{ i }}{% endfor %}', '56789'); equal('{% for i in range(-2, 0) %}{{ i }}{% endfor %}', '-2-1'); equal('{% for i in range(5, 10, 2) %}{{ i }}{% endfor %}', '579'); equal('{% for i in range(5, 10, 2.5) %}{{ i }}{% endfor %}', '57.5'); equal('{% for i in range(5, 10, 2.5) %}{{ i }}{% endfor %}', '57.5'); equal('{% for i in range(10, 5, -1) %}{{ i }}{% endfor %}', '109876'); equal('{% for i in range(10, 5, -2.5) %}{{ i }}{% endfor %}', '107.5'); finish(done); }); it('should have cycler', function(done) { equal( '{% set cls = cycler("odd", "even") %}' + '{{ cls.next() }}' + '{{ cls.next() }}' + '{{ cls.next() }}', 'oddevenodd'); equal( '{% set cls = cycler("odd", "even") %}' + '{{ cls.next() }}' + '{{ cls.reset() }}' + '{{ cls.next() }}', 'oddodd'); equal( '{% set cls = cycler("odd", "even") %}' + '{{ cls.next() }}' + '{{ cls.next() }}' + '{{ cls.current }}', 'oddeveneven'); finish(done); }); it('should have joiner', function(done) { equal( '{% set comma = joiner() %}' + 'foo{{ comma() }}bar{{ comma() }}baz{{ comma() }}', 'foobar,baz,'); equal( '{% set pipe = joiner("|") %}' + 'foo{{ pipe() }}bar{{ pipe() }}baz{{ pipe() }}', 'foobar|baz|'); finish(done); }); it('should allow addition of globals', function(done) { var env = new Environment(); env.addGlobal('hello', function(arg1) { return 'Hello ' + arg1; }); equal('{{ hello("World!") }}', 'Hello World!', env); finish(done); }); it('should allow chaining of globals', function(done) { var env = new Environment(); env.addGlobal('hello', function(arg1) { return 'Hello ' + arg1; }).addGlobal('goodbye', function(arg1) { return 'Goodbye ' + arg1; }); equal('{{ hello("World!") }}', 'Hello World!', env); equal('{{ goodbye("World!") }}', 'Goodbye World!', env); finish(done); }); it('should allow getting of globals', function(done) { var env = new Environment(); var hello = function(arg1) { return 'Hello ' + arg1; }; env.addGlobal('hello', hello); expect(env.getGlobal('hello')).to.be.equal(hello); finish(done); }); it('should allow getting boolean globals', function(done) { var env = new Environment(); var hello = false; env.addGlobal('hello', hello); expect(env.getGlobal('hello')).to.be.equal(hello); finish(done); }); it('should fail on getting non-existent global', function(done) { var env = new Environment(); // Using this format instead of .withArgs since env.getGlobal uses 'this' expect(function() { env.getGlobal('hello'); }).to.throwError(); finish(done); }); it('should pass context as this to global functions', function(done) { var env = new Environment(); env.addGlobal('hello', function() { return 'Hello ' + this.lookup('user'); }); equal('{{ hello() }}', { user: 'James' }, 'Hello James', env); finish(done); }); it('should be exclusive to each environment', function(done) { var env = new Environment(); var env2; env.addGlobal('hello', 'konichiwa'); env2 = new Environment(); // Using this format instead of .withArgs since env2.getGlobal uses 'this' expect(function() { env2.getGlobal('hello'); }).to.throwError(); finish(done); }); it('should return errors from globals', function(done) { var env = new Environment(); env.addGlobal('err', function() { throw new Error('Global error'); }); try { render('{{ err() }}', null, {}, env); } catch (e) { expect(e).to.be.a(Error); } finish(done); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/jinja-compat.js000664 000000 000000 00000006217 14012546311 024135 0ustar00rootroot000000 000000 (function() { 'use strict'; var util; var equal; var finish; if (typeof require !== 'undefined') { util = require('./util'); } else { util = window.util; } equal = util.jinjaEqual; finish = util.finish; describe('jinja-compat', function() { var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; it('should support array slices with start and stop', function(done) { equal('{% for i in arr[1:4] %}{{ i }}{% endfor %}', { arr: arr }, 'bcd'); finish(done); }); it('should support array slices using expressions', function(done) { equal('{% for i in arr[n:n+3] %}{{ i }}{% endfor %}', { n: 1, arr: arr }, 'bcd'); finish(done); }); it('should support array slices with start', function(done) { equal('{% for i in arr[3:] %}{{ i }}{% endfor %}', { arr: arr }, 'defgh'); finish(done); }); it('should support array slices with negative start', function(done) { equal('{% for i in arr[-3:] %}{{ i }}{% endfor %}', { arr: arr }, 'fgh'); finish(done); }); it('should support array slices with stop', function(done) { equal('{% for i in arr[:4] %}{{ i }}{% endfor %}', { arr: arr }, 'abcd'); finish(done); }); it('should support array slices with negative stop', function(done) { equal('{% for i in arr[:-3] %}{{ i }}{% endfor %}', { arr: arr }, 'abcde'); finish(done); }); it('should support array slices with step', function(done) { equal('{% for i in arr[::2] %}{{ i }}{% endfor %}', { arr: arr }, 'aceg'); finish(done); }); it('should support array slices with negative step', function(done) { equal('{% for i in arr[::-1] %}{{ i }}{% endfor %}', { arr: arr }, 'hgfedcba'); finish(done); }); it('should support array slices with start and negative step', function(done) { equal('{% for i in arr[4::-1] %}{{ i }}{% endfor %}', { arr: arr }, 'edcba'); finish(done); }); it('should support array slices with negative start and negative step', function(done) { equal('{% for i in arr[-5::-1] %}{{ i }}{% endfor %}', { arr: arr }, 'dcba'); finish(done); }); it('should support array slices with stop and negative step', function(done) { equal('{% for i in arr[:3:-1] %}{{ i }}{% endfor %}', { arr: arr }, 'hgfe'); finish(done); }); it('should support array slices with start and step', function(done) { equal('{% for i in arr[1::2] %}{{ i }}{% endfor %}', { arr: arr }, 'bdfh'); finish(done); }); it('should support array slices with start, stop, and step', function(done) { equal('{% for i in arr[1:7:2] %}{{ i }}{% endfor %}', { arr: arr }, 'bdf'); finish(done); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/lexer.js000664 000000 000000 00000044622 14012546311 022702 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect; var lib; var lexer; if (typeof require !== 'undefined') { expect = require('expect.js'); lib = require('../nunjucks/src/lib'); lexer = require('../nunjucks/src/lexer'); } else { expect = window.expect; lib = nunjucks.lib; lexer = nunjucks.lexer; } function _hasTokens(ws, tokens, types) { var i; var type; var tok; for (i = 0; i < types.length; i++) { type = types[i]; tok = tokens.nextToken(); if (!ws) { while (tok && tok.type === lexer.TOKEN_WHITESPACE) { tok = tokens.nextToken(); } } if (lib.isArray(type)) { expect(tok.type).to.be(type[0]); expect(tok.value).to.be(type[1]); } else if (lib.isObject(type)) { expect(tok.type).to.be(type.type); if (type.value != null) { expect(tok.value).to.be(type.value); } if (type.lineno != null) { expect(tok.lineno).to.be(type.lineno); } if (type.colno != null) { expect(tok.colno).to.be(type.colno); } } else { expect(tok.type).to.be(type); } } } function hasTokens(tokens /* , types */) { return _hasTokens(false, tokens, lib.toArray(arguments).slice(1)); } function hasTokensWithWS(tokens /* , types */) { return _hasTokens(true, tokens, lib.toArray(arguments).slice(1)); } describe('lexer', function() { var tok; var tmpl; var tokens; it('should parse template data', function() { tok = lexer.lex('3').nextToken(); expect(tok.type).to.be(lexer.TOKEN_DATA); expect(tok.value).to.be('3'); tmpl = 'foo bar bizzle 3 [1,2] !@#$%^&*()<>?:"{}|'; tok = lexer.lex(tmpl).nextToken(); expect(tok.type).to.be(lexer.TOKEN_DATA); expect(tok.value).to.be(tmpl); }); it('should keep track of whitespace', function() { tokens = lexer.lex('data {% 1 2\n 3 %} data'); hasTokensWithWS(tokens, lexer.TOKEN_DATA, lexer.TOKEN_BLOCK_START, [lexer.TOKEN_WHITESPACE, ' '], lexer.TOKEN_INT, [lexer.TOKEN_WHITESPACE, ' '], lexer.TOKEN_INT, [lexer.TOKEN_WHITESPACE, '\n '], lexer.TOKEN_INT, [lexer.TOKEN_WHITESPACE, ' '], lexer.TOKEN_BLOCK_END, lexer.TOKEN_DATA); }); it('should trim blocks', function() { tokens = lexer.lex(' {% if true %}\n foo\n {% endif %}\n', { trimBlocks: true }); hasTokens(tokens, [lexer.TOKEN_DATA, ' '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BOOLEAN, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, ' foo\n '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END); }); it('should trim windows-style CRLF line endings after blocks', function() { tokens = lexer.lex(' {% if true %}\r\n foo\r\n {% endif %}\r\n', { trimBlocks: true }); hasTokens(tokens, [lexer.TOKEN_DATA, ' '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BOOLEAN, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, ' foo\r\n '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END); }); it('should not trim CR after blocks', function() { tokens = lexer.lex(' {% if true %}\r foo\r\n {% endif %}\r', { trimBlocks: true }); hasTokens(tokens, [lexer.TOKEN_DATA, ' '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BOOLEAN, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, '\r foo\r\n '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, '\r']); }); it('should lstrip and trim blocks', function() { tokens = lexer.lex('test\n {% if true %}\n foo\n {% endif %}\n', { lstripBlocks: true, trimBlocks: true }); hasTokens(tokens, [lexer.TOKEN_DATA, 'test\n'], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BOOLEAN, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, ' foo\n'], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, '']); }); it('should lstrip and not collapse whitespace between blocks', function() { tokens = lexer.lex(' {% t %} {% t %}', { lstripBlocks: true }); hasTokens(tokens, lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END, [lexer.TOKEN_DATA, ' '], lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END); }); it('should parse variable start and end', function() { tokens = lexer.lex('data {{ foo }} bar bizzle'); hasTokens(tokens, lexer.TOKEN_DATA, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_VARIABLE_END, lexer.TOKEN_DATA); }); it('should treat the non-breaking space as valid whitespace', function() { tokens = lexer.lex('{{\u00A0foo }}'); tok = tokens.nextToken(); tok = tokens.nextToken(); tok = tokens.nextToken(); expect(tok.type).to.be(lexer.TOKEN_SYMBOL); expect(tok.value).to.be('foo'); }); it('should parse block start and end', function() { tokens = lexer.lex('data {% foo %} bar bizzle'); hasTokens(tokens, lexer.TOKEN_DATA, lexer.TOKEN_BLOCK_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_BLOCK_END, lexer.TOKEN_DATA); }); it('should parse basic types', function() { tokens = lexer.lex('{{ 3 4.5 true false none foo "hello" \'boo\' r/regex/ }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_INT, lexer.TOKEN_FLOAT, lexer.TOKEN_BOOLEAN, lexer.TOKEN_BOOLEAN, lexer.TOKEN_NONE, lexer.TOKEN_SYMBOL, lexer.TOKEN_STRING, lexer.TOKEN_STRING, lexer.TOKEN_REGEX, lexer.TOKEN_VARIABLE_END); }); it('should parse function calls', function() { tokens = lexer.lex('{{ foo(bar) }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, [lexer.TOKEN_SYMBOL, 'foo'], lexer.TOKEN_LEFT_PAREN, [lexer.TOKEN_SYMBOL, 'bar'], lexer.TOKEN_RIGHT_PAREN, lexer.TOKEN_VARIABLE_END); }); it('should parse groups', function() { tokens = lexer.lex('{{ (1, 2, 3) }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_LEFT_PAREN, lexer.TOKEN_INT, lexer.TOKEN_COMMA, lexer.TOKEN_INT, lexer.TOKEN_COMMA, lexer.TOKEN_INT, lexer.TOKEN_RIGHT_PAREN, lexer.TOKEN_VARIABLE_END); }); it('should parse arrays', function() { tokens = lexer.lex('{{ [1, 2, 3] }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_LEFT_BRACKET, lexer.TOKEN_INT, lexer.TOKEN_COMMA, lexer.TOKEN_INT, lexer.TOKEN_COMMA, lexer.TOKEN_INT, lexer.TOKEN_RIGHT_BRACKET, lexer.TOKEN_VARIABLE_END); }); it('should parse dicts', function() { tokens = lexer.lex('{{ {one:1, "two":2} }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_LEFT_CURLY, [lexer.TOKEN_SYMBOL, 'one'], lexer.TOKEN_COLON, [lexer.TOKEN_INT, '1'], lexer.TOKEN_COMMA, [lexer.TOKEN_STRING, 'two'], lexer.TOKEN_COLON, [lexer.TOKEN_INT, '2'], lexer.TOKEN_RIGHT_CURLY, lexer.TOKEN_VARIABLE_END); }); it('should parse blocks without whitespace', function() { tokens = lexer.lex('data{{hello}}{%if%}data'); hasTokens(tokens, lexer.TOKEN_DATA, lexer.TOKEN_VARIABLE_START, [lexer.TOKEN_SYMBOL, 'hello'], lexer.TOKEN_VARIABLE_END, lexer.TOKEN_BLOCK_START, [lexer.TOKEN_SYMBOL, 'if'], lexer.TOKEN_BLOCK_END, lexer.TOKEN_DATA); }); it('should parse filters', function() { hasTokens(lexer.lex('{{ foo|bar }}'), lexer.TOKEN_VARIABLE_START, [lexer.TOKEN_SYMBOL, 'foo'], lexer.TOKEN_PIPE, [lexer.TOKEN_SYMBOL, 'bar'], lexer.TOKEN_VARIABLE_END); }); it('should parse operators', function() { hasTokens(lexer.lex('{{ 3+3-3*3/3 }}'), lexer.TOKEN_VARIABLE_START, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_VARIABLE_END); hasTokens(lexer.lex('{{ 3**4//5 }}'), lexer.TOKEN_VARIABLE_START, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_VARIABLE_END); hasTokens(lexer.lex('{{ 3 != 4 == 5 <= 6 >= 7 < 8 > 9 }}'), lexer.TOKEN_VARIABLE_START, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_OPERATOR, lexer.TOKEN_INT, lexer.TOKEN_VARIABLE_END); }); it('should parse comments', function() { tokens = lexer.lex('data data {# comment #} data'); hasTokens(tokens, lexer.TOKEN_DATA, lexer.TOKEN_COMMENT, lexer.TOKEN_DATA); }); it('should allow changing the variable start and end', function() { tokens = lexer.lex('data {= var =}', { tags: { variableStart: '{=', variableEnd: '=}' } }); hasTokens(tokens, lexer.TOKEN_DATA, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_VARIABLE_END); }); it('should allow changing the block start and end', function() { tokens = lexer.lex('{= =}', { tags: { blockStart: '{=', blockEnd: '=}' } }); hasTokens(tokens, lexer.TOKEN_BLOCK_START, lexer.TOKEN_BLOCK_END); }); it('should allow changing the variable start and end', function() { tokens = lexer.lex('data {= var =}', { tags: { variableStart: '{=', variableEnd: '=}' } }); hasTokens(tokens, lexer.TOKEN_DATA, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_SYMBOL, lexer.TOKEN_VARIABLE_END); }); it('should allow changing the comment start and end', function() { tokens = lexer.lex('', { tags: { commentStart: '' } }); hasTokens(tokens, lexer.TOKEN_COMMENT); }); /** * Test that this bug is fixed: https://github.com/mozilla/nunjucks/issues/235 */ it('should have individual lexer tag settings for each environment', function() { tokens = lexer.lex('{=', { tags: { variableStart: '{=' } }); hasTokens(tokens, lexer.TOKEN_VARIABLE_START); tokens = lexer.lex('{{'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START); tokens = lexer.lex('{{', { tags: { variableStart: '<<<' } }); hasTokens(tokens, lexer.TOKEN_DATA); tokens = lexer.lex('{{'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START); }); it('should parse regular expressions', function() { tokens = lexer.lex('{{ r/basic regex [a-z]/ }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_REGEX, lexer.TOKEN_VARIABLE_END); // A more complex regex with escaped slashes. tokens = lexer.lex('{{ r/{a*b} \\/regex! [0-9]\\// }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_REGEX, lexer.TOKEN_VARIABLE_END); // This one has flags. tokens = lexer.lex('{{ r/^x/gim }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_REGEX, lexer.TOKEN_VARIABLE_END); // This one has a valid flag then an invalid flag. tokens = lexer.lex('{{ r/x$/iv }}'); hasTokens(tokens, lexer.TOKEN_VARIABLE_START, lexer.TOKEN_REGEX, lexer.TOKEN_SYMBOL, lexer.TOKEN_VARIABLE_END); }); it('should keep track of token positions', function() { hasTokens(lexer.lex('{{ 3 != 4 == 5 <= 6 >= 7 < 8 > 9 }}'), { type: lexer.TOKEN_VARIABLE_START, lineno: 0, colno: 0, }, { type: lexer.TOKEN_INT, value: '3', lineno: 0, colno: 3, }, { type: lexer.TOKEN_OPERATOR, value: '!=', lineno: 0, colno: 5, }, { type: lexer.TOKEN_INT, value: '4', lineno: 0, colno: 8, }, { type: lexer.TOKEN_OPERATOR, value: '==', lineno: 0, colno: 10, }, { type: lexer.TOKEN_INT, value: '5', lineno: 0, colno: 13, }, { type: lexer.TOKEN_OPERATOR, value: '<=', lineno: 0, colno: 15, }, { type: lexer.TOKEN_INT, value: '6', lineno: 0, colno: 18, }, { type: lexer.TOKEN_OPERATOR, lineno: 0, colno: 20, value: '>=', }, { type: lexer.TOKEN_INT, lineno: 0, colno: 23, value: '7', }, { type: lexer.TOKEN_OPERATOR, value: '<', lineno: 0, colno: 25, }, { type: lexer.TOKEN_INT, value: '8', lineno: 0, colno: 27, }, { type: lexer.TOKEN_OPERATOR, value: '>', lineno: 0, colno: 29, }, { type: lexer.TOKEN_INT, value: '9', lineno: 0, colno: 31, }, { type: lexer.TOKEN_VARIABLE_END, lineno: 0, colno: 33, }); hasTokens(lexer.lex('{% if something %}{{ value }}{% else %}{{ otherValue }}{% endif %}'), { type: lexer.TOKEN_BLOCK_START, lineno: 0, colno: 0, }, { type: lexer.TOKEN_SYMBOL, value: 'if', lineno: 0, colno: 3, }, { type: lexer.TOKEN_SYMBOL, value: 'something', lineno: 0, colno: 6, }, { type: lexer.TOKEN_BLOCK_END, lineno: 0, colno: 16, }, { type: lexer.TOKEN_VARIABLE_START, lineno: 0, colno: 18, }, { type: lexer.TOKEN_SYMBOL, value: 'value', lineno: 0, colno: 21, }, { type: lexer.TOKEN_VARIABLE_END, lineno: 0, colno: 27, }, { type: lexer.TOKEN_BLOCK_START, lineno: 0, colno: 29, }, { type: lexer.TOKEN_SYMBOL, value: 'else', lineno: 0, colno: 32, }, { type: lexer.TOKEN_BLOCK_END, lineno: 0, colno: 37, }, { type: lexer.TOKEN_VARIABLE_START, lineno: 0, colno: 39, }, { type: lexer.TOKEN_SYMBOL, value: 'otherValue', lineno: 0, colno: 42, }, { type: lexer.TOKEN_VARIABLE_END, lineno: 0, colno: 53, }, { type: lexer.TOKEN_BLOCK_START, lineno: 0, colno: 55, }, { type: lexer.TOKEN_SYMBOL, value: 'endif', lineno: 0, colno: 58, }, { type: lexer.TOKEN_BLOCK_END, lineno: 0, colno: 64, }); hasTokens(lexer.lex('{% if something %}\n{{ value }}\n{% else %}\n{{ otherValue }}\n{% endif %}'), { type: lexer.TOKEN_BLOCK_START, lineno: 0, colno: 0, }, { type: lexer.TOKEN_SYMBOL, value: 'if', lineno: 0, colno: 3, }, { type: lexer.TOKEN_SYMBOL, value: 'something', lineno: 0, colno: 6, }, { type: lexer.TOKEN_BLOCK_END, lineno: 0, colno: 16, }, { type: lexer.TOKEN_DATA, value: '\n', }, { type: lexer.TOKEN_VARIABLE_START, lineno: 1, colno: 0, }, { type: lexer.TOKEN_SYMBOL, value: 'value', lineno: 1, colno: 3, }, { type: lexer.TOKEN_VARIABLE_END, lineno: 1, colno: 9, }, { type: lexer.TOKEN_DATA, value: '\n', }, { type: lexer.TOKEN_BLOCK_START, lineno: 2, colno: 0, }, { type: lexer.TOKEN_SYMBOL, value: 'else', lineno: 2, colno: 3, }, { type: lexer.TOKEN_BLOCK_END, lineno: 2, colno: 8, }, { type: lexer.TOKEN_DATA, value: '\n', }, { type: lexer.TOKEN_VARIABLE_START, lineno: 3, colno: 0, }, { type: lexer.TOKEN_SYMBOL, value: 'otherValue', lineno: 3, colno: 3, }, { type: lexer.TOKEN_VARIABLE_END, lineno: 3, colno: 14, }, { type: lexer.TOKEN_DATA, value: '\n', }, { type: lexer.TOKEN_BLOCK_START, lineno: 4, colno: 0, }, { type: lexer.TOKEN_SYMBOL, value: 'endif', lineno: 4, colno: 3, }, { type: lexer.TOKEN_BLOCK_END, lineno: 4, colno: 9, }); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/loader.js000664 000000 000000 00000011520 14012546311 023020 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect, Environment, WebLoader, FileSystemLoader, NodeResolveLoader, templatesPath; if (typeof require !== 'undefined') { expect = require('expect.js'); Environment = require('../nunjucks/src/environment').Environment; WebLoader = require('../nunjucks/src/web-loaders').WebLoader; FileSystemLoader = require('../nunjucks/src/node-loaders').FileSystemLoader; NodeResolveLoader = require('../nunjucks/src/node-loaders').NodeResolveLoader; templatesPath = 'tests/templates'; } else { expect = window.expect; Environment = nunjucks.Environment; WebLoader = nunjucks.WebLoader; FileSystemLoader = nunjucks.FileSystemLoader; NodeResolveLoader = nunjucks.NodeResolveLoader; templatesPath = '../templates'; } describe('loader', function() { it('should allow a simple loader to be created', function() { // From Docs: http://mozilla.github.io/nunjucks/api.html#writing-a-loader // We should be able to create a loader that only exposes getSource var env, parent; function MyLoader() { // configuration } MyLoader.prototype.getSource = function() { return { src: 'Hello World', path: '/tmp/somewhere' }; }; env = new Environment(new MyLoader(templatesPath)); parent = env.getTemplate('fake.njk'); expect(parent.render()).to.be('Hello World'); }); it('should catch loader error', function(done) { // From Docs: http://mozilla.github.io/nunjucks/api.html#writing-a-loader // We should be able to create a loader that only exposes getSource var env; function MyLoader() { // configuration this.async = true; } MyLoader.prototype.getSource = function(s, cb) { setTimeout(function() { cb(new Error('test')); }, 1); }; env = new Environment(new MyLoader(templatesPath)); env.getTemplate('fake.njk', function(err, parent) { expect(err).to.be.a(Error); expect(parent).to.be(undefined); done(); }); }); describe('WebLoader', function() { it('should have default opts for WebLoader', function() { var webLoader = new WebLoader(templatesPath); expect(webLoader).to.be.a(WebLoader); expect(webLoader.useCache).to.be(false); expect(webLoader.async).to.be(false); }); it('should emit a "load" event', function(done) { var loader = new WebLoader(templatesPath); if (typeof window === 'undefined') { this.skip(); } loader.on('load', function(name, source) { expect(name).to.equal('simple-base.njk'); done(); }); loader.getSource('simple-base.njk'); }); }); if (typeof FileSystemLoader !== 'undefined') { describe('FileSystemLoader', function() { it('should have default opts', function() { var loader = new FileSystemLoader(templatesPath); expect(loader).to.be.a(FileSystemLoader); expect(loader.noCache).to.be(false); }); it('should emit a "load" event', function(done) { var loader = new FileSystemLoader(templatesPath); loader.on('load', function(name, source) { expect(name).to.equal('simple-base.njk'); done(); }); loader.getSource('simple-base.njk'); }); }); } if (typeof NodeResolveLoader !== 'undefined') { describe('NodeResolveLoader', function() { it('should have default opts', function() { var loader = new NodeResolveLoader(); expect(loader).to.be.a(NodeResolveLoader); expect(loader.noCache).to.be(false); }); it('should emit a "load" event', function(done) { var loader = new NodeResolveLoader(); loader.on('load', function(name, source) { expect(name).to.equal('dummy-pkg/simple-template.html'); done(); }); loader.getSource('dummy-pkg/simple-template.html'); }); it('should render templates', function() { var env = new Environment(new NodeResolveLoader()); var tmpl = env.getTemplate('dummy-pkg/simple-template.html'); expect(tmpl.render({foo: 'foo'})).to.be('foo'); }); it('should not allow directory traversal', function() { var loader = new NodeResolveLoader(); var dummyPkgPath = require.resolve('dummy-pkg/simple-template.html'); expect(loader.getSource(dummyPkgPath)).to.be(null); }); it('should return null if no match', function() { var loader = new NodeResolveLoader(); var tmplName = 'dummy-pkg/does-not-exist.html'; expect(loader.getSource(tmplName)).to.be(null); }); }); } }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/parser.js000664 000000 000000 00000074704 14012546311 023063 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect, lib, nodes, parser; if (typeof require !== 'undefined') { expect = require('expect.js'); lib = require('../nunjucks/src/lib'); nodes = require('../nunjucks/src/nodes'); parser = require('../nunjucks/src/parser'); } else { expect = window.expect; lib = nunjucks.lib; nodes = nunjucks.nodes; parser = nunjucks.parser; } function _isAST(node1, node2) { // Compare ASTs // TODO: Clean this up (seriously, really) /* eslint-disable vars-on-top */ expect(node1.typename).to.be(node2.typename); if (node2 instanceof nodes.NodeList) { var lit = ': num-children: '; var sig2 = (node2.typename + lit + node2.children.length); expect(node1.children).to.be.ok(); var sig1 = (node1.typename + lit + node1.children.length); expect(sig1).to.be(sig2); for (var n = 0, l = node2.children.length; n < l; n++) { _isAST(node1.children[n], node2.children[n]); } } else { node2.iterFields(function(value, field) { var ofield = node1[field]; if (value instanceof nodes.Node) { _isAST(ofield, value); } else if (lib.isArray(ofield) && lib.isArray(value)) { expect('num-children: ' + ofield.length).to.be('num-children: ' + value.length); lib.each(ofield, function(v, i) { if (ofield[i] instanceof nodes.Node) { _isAST(ofield[i], value[i]); } else if (ofield[i] !== null && value[i] !== null) { expect(ofield[i]).to.be(value[i]); } }); } else if ((ofield !== null || value !== null) && (ofield !== undefined || value !== undefined)) { if (ofield === null) { throw new Error(value + ' expected for "' + field + '", null found'); } if (value === null) { throw new Error(ofield + ' expected to be null for "' + field + '"'); } // We want good errors and tracebacks, so test on // whichever object exists if (!ofield) { expect(value).to.be(ofield); } else if (ofield !== null && ofield instanceof RegExp) { // This conditional check for RegExp is needed because /a/ != /a/ expect(String(ofield)).to.be(String(value)); } else { expect(ofield).to.be(value); } } }); } } function isAST(node1, ast) { // Compare the ASTs, the second one is an AST literal so transform // it into a real one return _isAST(node1, toNodes(ast)); } // We'll be doing a lot of AST comparisons, so this defines a kind // of "AST literal" that you can specify with arrays. This // transforms it into a real AST. function toNodes(ast) { if (!(ast && lib.isArray(ast))) { return ast; } var Type = ast[0]; // some nodes have fields (e.g. Compare.ops) which are plain arrays if (Type instanceof Array) { return lib.map(ast, toNodes); } var F = function() {}; F.prototype = Type.prototype; var dummy = new F(); if (dummy instanceof nodes.NodeList) { return new Type(0, 0, lib.map(ast.slice(1), toNodes)); } else if (dummy instanceof nodes.CallExtension) { return new Type(ast[1], ast[2], ast[3] ? toNodes(ast[3]) : ast[3], lib.isArray(ast[4]) ? lib.map(ast[4], toNodes) : ast[4]); } else { return new Type(0, 0, toNodes(ast[1]), toNodes(ast[2]), toNodes(ast[3]), toNodes(ast[4]), toNodes(ast[5])); } } describe('parser', function() { it('should parse basic types', function() { isAST(parser.parse('{{ 1 }}'), [nodes.Root, [nodes.Output, [nodes.Literal, 1]]]); isAST(parser.parse('{{ 4.567 }}'), [nodes.Root, [nodes.Output, [nodes.Literal, 4.567]]]); isAST(parser.parse('{{ "foo" }}'), [nodes.Root, [nodes.Output, [nodes.Literal, 'foo']]]); isAST(parser.parse('{{ \'foo\' }}'), [nodes.Root, [nodes.Output, [nodes.Literal, 'foo']]]); isAST(parser.parse('{{ true }}'), [nodes.Root, [nodes.Output, [nodes.Literal, true]]]); isAST(parser.parse('{{ false }}'), [nodes.Root, [nodes.Output, [nodes.Literal, false]]]); isAST(parser.parse('{{ none }}'), [nodes.Root, [nodes.Output, [nodes.Literal, null]]]); isAST(parser.parse('{{ foo }}'), [nodes.Root, [nodes.Output, [nodes.Symbol, 'foo']]]); isAST(parser.parse('{{ r/23/gi }}'), [nodes.Root, [nodes.Output, [nodes.Literal, new RegExp('23', 'gi')]]]); }); it('should parse aggregate types', function() { isAST(parser.parse('{{ [1,2,3] }}'), [nodes.Root, [nodes.Output, [nodes.Array, [nodes.Literal, 1], [nodes.Literal, 2], [nodes.Literal, 3]]]]); isAST(parser.parse('{{ (1,2,3) }}'), [nodes.Root, [nodes.Output, [nodes.Group, [nodes.Literal, 1], [nodes.Literal, 2], [nodes.Literal, 3]]]]); isAST(parser.parse('{{ {foo: 1, \'two\': 2} }}'), [nodes.Root, [nodes.Output, [nodes.Dict, [nodes.Pair, [nodes.Symbol, 'foo'], [nodes.Literal, 1]], [nodes.Pair, [nodes.Literal, 'two'], [nodes.Literal, 2]]]]]); }); it('should parse variables', function() { isAST(parser.parse('hello {{ foo }}, how are you'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'hello ']], [nodes.Output, [nodes.Symbol, 'foo']], [nodes.Output, [nodes.TemplateData, ', how are you']]]); }); it('should parse operators', function() { isAST(parser.parse('{{ x == y }}'), [nodes.Root, [nodes.Output, [nodes.Compare, [nodes.Symbol, 'x'], [[nodes.CompareOperand, [nodes.Symbol, 'y'], '==']]]]]); isAST(parser.parse('{{ x or y }}'), [nodes.Root, [nodes.Output, [nodes.Or, [nodes.Symbol, 'x'], [nodes.Symbol, 'y']]]]); isAST(parser.parse('{{ x in y }}'), [nodes.Root, [nodes.Output, [nodes.In, [nodes.Symbol, 'x'], [nodes.Symbol, 'y']]]]); isAST(parser.parse('{{ x not in y }}'), [nodes.Root, [nodes.Output, [nodes.Not, [nodes.In, [nodes.Symbol, 'x'], [nodes.Symbol, 'y']]]]]); isAST(parser.parse('{{ x is callable }}'), [nodes.Root, [nodes.Output, [nodes.Is, [nodes.Symbol, 'x'], [nodes.Symbol, 'callable']]]]); isAST(parser.parse('{{ x is not callable }}'), [nodes.Root, [nodes.Output, [nodes.Not, [nodes.Is, [nodes.Symbol, 'x'], [nodes.Symbol, 'callable']]]]]); }); it('should parse tilde', function() { isAST(parser.parse('{{ 2 ~ 3 }}'), [nodes.Root, [nodes.Output, [nodes.Concat, [nodes.Literal, 2], [nodes.Literal, 3] ]]] ); }); it('should parse operators with correct precedence', function() { isAST(parser.parse('{{ x in y and z }}'), [nodes.Root, [nodes.Output, [nodes.And, [nodes.In, [nodes.Symbol, 'x'], [nodes.Symbol, 'y']], [nodes.Symbol, 'z']]]]); isAST(parser.parse('{{ x not in y or z }}'), [nodes.Root, [nodes.Output, [nodes.Or, [nodes.Not, [nodes.In, [nodes.Symbol, 'x'], [nodes.Symbol, 'y']]], [nodes.Symbol, 'z']]]]); isAST(parser.parse('{{ x or y and z }}'), [nodes.Root, [nodes.Output, [nodes.Or, [nodes.Symbol, 'x'], [nodes.And, [nodes.Symbol, 'y'], [nodes.Symbol, 'z']]]]]); }); it('should parse blocks', function() { var n = parser.parse('want some {% if hungry %}pizza{% else %}' + 'water{% endif %}?'); expect(n.children[1].typename).to.be('If'); n = parser.parse('{% block foo %}stuff{% endblock %}'); expect(n.children[0].typename).to.be('Block'); n = parser.parse('{% block foo %}stuff{% endblock foo %}'); expect(n.children[0].typename).to.be('Block'); n = parser.parse('{% extends "test.njk" %}stuff'); expect(n.children[0].typename).to.be('Extends'); n = parser.parse('{% include "test.njk" %}'); expect(n.children[0].typename).to.be('Include'); }); it('should accept attributes and methods of static arrays, objects and primitives', function() { expect(function() { parser.parse('{{ ([1, 2, 3]).indexOf(1) }}'); }).to.not.throwException(); expect(function() { parser.parse('{{ [1, 2, 3].length }}'); }).to.not.throwException(); expect(function() { parser.parse('{{ "Some String".replace("S", "$") }}'); }).to.not.throwException(); expect(function() { parser.parse('{{ ({ name : "Khalid" }).name }}'); }).to.not.throwException(); expect(function() { parser.parse('{{ 1.618.toFixed(2) }}'); }).to.not.throwException(); }); it('should parse include tags', function() { var n = parser.parse('{% include "test.njk" %}'); expect(n.children[0].typename).to.be('Include'); n = parser.parse('{% include "test.html"|replace("html","j2") %}'); expect(n.children[0].typename).to.be('Include'); n = parser.parse('{% include ""|default("test.njk") %}'); expect(n.children[0].typename).to.be('Include'); }); it('should parse for loops', function() { isAST(parser.parse('{% for x in [1, 2] %}{{ x }}{% endfor %}'), [nodes.Root, [nodes.For, [nodes.Array, [nodes.Literal, 1], [nodes.Literal, 2]], [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.Symbol, 'x']]]]]); }); it('should parse for loops with else', function() { isAST(parser.parse('{% for x in [] %}{{ x }}{% else %}empty{% endfor %}'), [nodes.Root, [nodes.For, [nodes.Array], [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.Symbol, 'x']]], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'empty']]]]]); }); it('should parse filters', function() { isAST(parser.parse('{{ foo | bar }}'), [nodes.Root, [nodes.Output, [nodes.Filter, [nodes.Symbol, 'bar'], [nodes.NodeList, [nodes.Symbol, 'foo']]]]]); isAST(parser.parse('{{ foo | bar | baz }}'), [nodes.Root, [nodes.Output, [nodes.Filter, [nodes.Symbol, 'baz'], [nodes.NodeList, [nodes.Filter, [nodes.Symbol, 'bar'], [nodes.NodeList, [nodes.Symbol, 'foo']]]]]]]); isAST(parser.parse('{{ foo | bar(3) }}'), [nodes.Root, [nodes.Output, [nodes.Filter, [nodes.Symbol, 'bar'], [nodes.NodeList, [nodes.Symbol, 'foo'], [nodes.Literal, 3]]]]]); }); it('should parse macro definitions', function() { var ast = parser.parse('{% macro foo(bar, baz="foobar") %}' + 'This is a macro' + '{% endmacro %}'); isAST(ast, [nodes.Root, [nodes.Macro, [nodes.Symbol, 'foo'], [nodes.NodeList, [nodes.Symbol, 'bar'], [nodes.KeywordArgs, [nodes.Pair, [nodes.Symbol, 'baz'], [nodes.Literal, 'foobar']]]], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'This is a macro']]]]]); }); it('should parse call blocks', function() { var ast = parser.parse('{% call foo("bar") %}' + 'This is the caller' + '{% endcall %}'); isAST(ast, [nodes.Root, [nodes.Output, [nodes.FunCall, [nodes.Symbol, 'foo'], [nodes.NodeList, [nodes.Literal, 'bar'], [nodes.KeywordArgs, [nodes.Pair, [nodes.Symbol, 'caller'], [nodes.Caller, [nodes.Symbol, 'caller'], [nodes.NodeList], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'This is the caller']]]]]]]]]]); }); it('should parse call blocks with args', function() { var ast = parser.parse('{% call(i) foo("bar", baz="foobar") %}' + 'This is {{ i }}' + '{% endcall %}'); isAST(ast, [nodes.Root, [nodes.Output, [nodes.FunCall, [nodes.Symbol, 'foo'], [nodes.NodeList, [nodes.Literal, 'bar'], [nodes.KeywordArgs, [nodes.Pair, [nodes.Symbol, 'baz'], [nodes.Literal, 'foobar']], [nodes.Pair, [nodes.Symbol, 'caller'], [nodes.Caller, [nodes.Symbol, 'caller'], [nodes.NodeList, [nodes.Symbol, 'i']], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'This is ']], [nodes.Output, [nodes.Symbol, 'i']]]]]]]]]]); }); it('should parse raw', function() { isAST(parser.parse('{% raw %}hello {{ {% %} }}{% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'hello {{ {% %} }}']]]); }); it('should parse raw with broken variables', function() { isAST(parser.parse('{% raw %}{{ x }{% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{{ x }']]]); }); it('should parse raw with broken blocks', function() { isAST(parser.parse('{% raw %}{% if i_am_stupid }Still do your job well{% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{% if i_am_stupid }Still do your job well']]]); }); it('should parse raw with pure text', function() { isAST(parser.parse('{% raw %}abc{% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'abc']]]); }); it('should parse raw with raw blocks', function() { isAST(parser.parse('{% raw %}{% raw %}{{ x }{% endraw %}{% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{% raw %}{{ x }{% endraw %}']]]); }); it('should parse raw with comment blocks', function() { isAST(parser.parse('{% raw %}{# test {% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{# test ']]]); }); it('should parse multiple raw blocks', function() { isAST(parser.parse('{% raw %}{{ var }}{% endraw %}{{ var }}{% raw %}{{ var }}{% endraw %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{{ var }}']], [nodes.Output, [nodes.Symbol, 'var']], [nodes.Output, [nodes.TemplateData, '{{ var }}']]]); }); it('should parse multiline multiple raw blocks', function() { isAST(parser.parse('\n{% raw %}{{ var }}{% endraw %}\n{{ var }}\n{% raw %}{{ var }}{% endraw %}\n'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '\n']], [nodes.Output, [nodes.TemplateData, '{{ var }}']], [nodes.Output, [nodes.TemplateData, '\n']], [nodes.Output, [nodes.Symbol, 'var']], [nodes.Output, [nodes.TemplateData, '\n']], [nodes.Output, [nodes.TemplateData, '{{ var }}']], [nodes.Output, [nodes.TemplateData, '\n']]]); }); it('should parse verbatim', function() { isAST(parser.parse('{% verbatim %}hello {{ {% %} }}{% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'hello {{ {% %} }}']]]); }); it('should parse verbatim with broken variables', function() { isAST(parser.parse('{% verbatim %}{{ x }{% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{{ x }']]]); }); it('should parse verbatim with broken blocks', function() { isAST(parser.parse('{% verbatim %}{% if i_am_stupid }Still do your job well{% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{% if i_am_stupid }Still do your job well']]]); }); it('should parse verbatim with pure text', function() { isAST(parser.parse('{% verbatim %}abc{% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'abc']]]); }); it('should parse verbatim with verbatim blocks', function() { isAST(parser.parse('{% verbatim %}{% verbatim %}{{ x }{% endverbatim %}{% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{% verbatim %}{{ x }{% endverbatim %}']]]); }); it('should parse verbatim with comment blocks', function() { isAST(parser.parse('{% verbatim %}{# test {% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{# test ']]]); }); it('should parse multiple verbatim blocks', function() { isAST(parser.parse('{% verbatim %}{{ var }}{% endverbatim %}{{ var }}{% verbatim %}{{ var }}{% endverbatim %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '{{ var }}']], [nodes.Output, [nodes.Symbol, 'var']], [nodes.Output, [nodes.TemplateData, '{{ var }}']]]); }); it('should parse multiline multiple verbatim blocks', function() { isAST(parser.parse('\n{% verbatim %}{{ var }}{% endverbatim %}\n{{ var }}\n{% verbatim %}{{ var }}{% endverbatim %}\n'), [nodes.Root, [nodes.Output, [nodes.TemplateData, '\n']], [nodes.Output, [nodes.TemplateData, '{{ var }}']], [nodes.Output, [nodes.TemplateData, '\n']], [nodes.Output, [nodes.Symbol, 'var']], [nodes.Output, [nodes.TemplateData, '\n']], [nodes.Output, [nodes.TemplateData, '{{ var }}']], [nodes.Output, [nodes.TemplateData, '\n']]]); }); it('should parse switch statements', function() { var tpl = '{% switch foo %}{% case "bar" %}BAR{% case "baz" %}BAZ{% default %}NEITHER FOO NOR BAR{% endswitch %}'; isAST(parser.parse(tpl), [nodes.Root, [nodes.Switch, [nodes.Symbol, 'foo'], [ [nodes.Case, [nodes.Literal, 'bar'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'BAR']]]], [nodes.Case, [nodes.Literal, 'baz'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'BAZ']]]]], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'NEITHER FOO NOR BAR']]]]]); }); it('should parse keyword and non-keyword arguments', function() { isAST(parser.parse('{{ foo("bar", falalalala, baz="foobar") }}'), [nodes.Root, [nodes.Output, [nodes.FunCall, [nodes.Symbol, 'foo'], [nodes.NodeList, [nodes.Literal, 'bar'], [nodes.Symbol, 'falalalala'], [nodes.KeywordArgs, [nodes.Pair, [nodes.Symbol, 'baz'], [nodes.Literal, 'foobar']]]]]]]); }); it('should parse imports', function() { isAST(parser.parse('{% import "foo/bar.njk" as baz %}'), [nodes.Root, [nodes.Import, [nodes.Literal, 'foo/bar.njk'], [nodes.Symbol, 'baz']]]); isAST(parser.parse('{% from "foo/bar.njk" import baz, foobar as foobarbaz %}'), [nodes.Root, [nodes.FromImport, [nodes.Literal, 'foo/bar.njk'], [nodes.NodeList, [nodes.Symbol, 'baz'], [nodes.Pair, [nodes.Symbol, 'foobar'], [nodes.Symbol, 'foobarbaz']]]]]); isAST(parser.parse('{% import "foo/bar.html"|replace("html", "j2") as baz %}'), [nodes.Root, [nodes.Import, [nodes.Filter, [nodes.Symbol, 'replace'], [nodes.NodeList, [nodes.Literal, 'foo/bar.html'], [nodes.Literal, 'html'], [nodes.Literal, 'j2'] ] ], [nodes.Symbol, 'baz']]]); isAST(parser.parse('{% from ""|default("foo/bar.njk") import baz, foobar as foobarbaz %}'), [nodes.Root, [nodes.FromImport, [nodes.Filter, [nodes.Symbol, 'default'], [nodes.NodeList, [nodes.Literal, ''], [nodes.Literal, 'foo/bar.njk'] ] ], [nodes.NodeList, [nodes.Symbol, 'baz'], [nodes.Pair, [nodes.Symbol, 'foobar'], [nodes.Symbol, 'foobarbaz']]]]]); }); it('should parse whitespace control', function() { // Every start/end tag with "-" should trim the whitespace // before or after it isAST(parser.parse('{% if x %}\n hi \n{% endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, '\n hi \n']]]]]); isAST(parser.parse('{% if x -%}\n hi \n{% endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'hi \n']]]]]); isAST(parser.parse('{% if x %}\n hi \n{%- endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, '\n hi']]]]]); isAST(parser.parse('{% if x -%}\n hi \n{%- endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'hi']]]]]); isAST(parser.parse('poop \n{%- if x -%}\n hi \n{%- endif %}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'poop']], [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'hi']]]]]); isAST(parser.parse('hello \n{#- comment #}'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'hello']]]); isAST(parser.parse('{# comment -#} \n world'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'world']]]); isAST(parser.parse('hello \n{#- comment -#} \n world'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'hello']], [nodes.Output, [nodes.TemplateData, 'world']]]); isAST(parser.parse('hello \n{# - comment - #} \n world'), [nodes.Root, [nodes.Output, [nodes.TemplateData, 'hello \n']], [nodes.Output, [nodes.TemplateData, ' \n world']]]); // The from statement required a special case so make sure to // test it isAST(parser.parse('{% from x import y %}\n hi \n'), [nodes.Root, [nodes.FromImport, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Symbol, 'y']]], [nodes.Output, [nodes.TemplateData, '\n hi \n']]]); isAST(parser.parse('{% from x import y -%}\n hi \n'), [nodes.Root, [nodes.FromImport, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Symbol, 'y']]], [nodes.Output, [nodes.TemplateData, 'hi \n']]]); isAST(parser.parse('{% if x -%}{{y}} {{z}}{% endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, [nodes.Symbol, 'y']], [nodes.Output, // the value of TemplateData should be ' ' instead of '' [nodes.TemplateData, ' ']], [nodes.Output, [nodes.Symbol, 'z']]]]]); isAST(parser.parse('{% if x -%}{% if y %} {{z}}{% endif %}{% endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.If, [nodes.Symbol, 'y'], [nodes.NodeList, [nodes.Output, // the value of TemplateData should be ' ' instead of '' [nodes.TemplateData, ' ']], [nodes.Output, [nodes.Symbol, 'z']] ]]]]]); isAST(parser.parse('{% if x -%}{# comment #} {{z}}{% endif %}'), [nodes.Root, [nodes.If, [nodes.Symbol, 'x'], [nodes.NodeList, [nodes.Output, // the value of TemplateData should be ' ' instead of '' [nodes.TemplateData, ' ']], [nodes.Output, [nodes.Symbol, 'z']]]]]); }); it('should throw errors', function() { expect(function() { parser.parse('hello {{ foo'); }).to.throwException(/expected variable end/); expect(function() { parser.parse('hello {% if'); }).to.throwException(/expected expression/); expect(function() { parser.parse('hello {% if sdf zxc'); }).to.throwException(/expected block end/); expect(function() { parser.parse('{% include "foo %}'); }).to.throwException(/expected block end/); expect(function() { parser.parse('hello {% if sdf %} data'); }).to.throwException(/expected elif, else, or endif/); expect(function() { parser.parse('hello {% block sdf %} data'); }).to.throwException(/expected endblock/); expect(function() { parser.parse('hello {% block sdf %} data{% endblock foo %}'); }).to.throwException(/expected block end/); expect(function() { parser.parse('hello {% bar %} dsfsdf'); }).to.throwException(/unknown block tag/); expect(function() { parser.parse('{{ foo(bar baz) }}'); }).to.throwException(/expected comma after expression/); expect(function() { parser.parse('{% import "foo" %}'); }).to.throwException(/expected "as" keyword/); expect(function() { parser.parse('{% from "foo" %}'); }).to.throwException(/expected import/); expect(function() { parser.parse('{% from "foo" import bar baz %}'); }).to.throwException(/expected comma/); expect(function() { parser.parse('{% from "foo" import _bar %}'); }).to.throwException(/names starting with an underscore cannot be imported/); }); it('should parse custom tags', function() { function TestTagExtension() { /* eslint-disable no-shadow */ this.tags = ['testtag']; /* normally this is automatically done by Environment */ this._name = 'testtagExtension'; this.parse = function(parser, nodes) { parser.peekToken(); parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, 'foo'); }; } function TestBlockTagExtension() { /* eslint-disable no-shadow */ this.tags = ['testblocktag']; this._name = 'testblocktagExtension'; this.parse = function(parser, nodes) { parser.peekToken(); parser.advanceAfterBlockEnd(); var content = parser.parseUntilBlocks('endtestblocktag'); var tag = new nodes.CallExtension(this, 'bar', null, [1, content]); parser.advanceAfterBlockEnd(); return tag; }; } function TestArgsExtension() { /* eslint-disable no-shadow */ this.tags = ['testargs']; this._name = 'testargsExtension'; this.parse = function(parser, nodes) { var begun = parser.peekToken(); var args = null; // Skip the name parser.nextToken(); args = parser.parseSignature(true); parser.advanceAfterBlockEnd(begun.value); return new nodes.CallExtension(this, 'biz', args); }; } var extensions = [new TestTagExtension(), new TestBlockTagExtension(), new TestArgsExtension()]; isAST(parser.parse('{% testtag %}', extensions), [nodes.Root, [nodes.CallExtension, extensions[0], 'foo', undefined, undefined]]); isAST(parser.parse('{% testblocktag %}sdfd{% endtestblocktag %}', extensions), [nodes.Root, [nodes.CallExtension, extensions[1], 'bar', null, [1, [nodes.NodeList, [nodes.Output, [nodes.TemplateData, 'sdfd']]]]]]); isAST(parser.parse('{% testblocktag %}{{ 123 }}{% endtestblocktag %}', extensions), [nodes.Root, [nodes.CallExtension, extensions[1], 'bar', null, [1, [nodes.NodeList, [nodes.Output, [nodes.Literal, 123]]]]]]); isAST(parser.parse('{% testargs(123, "abc", foo="bar") %}', extensions), [nodes.Root, [nodes.CallExtension, extensions[2], 'biz', // The only arg is the list of run-time arguments // coming from the template [nodes.NodeList, [nodes.Literal, 123], [nodes.Literal, 'abc'], [nodes.KeywordArgs, [nodes.Pair, [nodes.Symbol, 'foo'], [nodes.Literal, 'bar']]]]]]); isAST(parser.parse('{% testargs %}', extensions), [nodes.Root, [nodes.CallExtension, extensions[2], 'biz', null]]); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/precompile.js000664 000000 000000 00000002567 14012546311 023724 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect, precompile, precompileString; if (typeof require !== 'undefined') { expect = require('expect.js'); precompile = require('../nunjucks/src/precompile').precompile; precompileString = require('../nunjucks/src/precompile').precompileString; } else { expect = window.expect; precompile = nunjucks.precompile; precompileString = nunjucks.precompileString; } describe('precompile', function() { it('should return a string', function() { expect(precompileString('{{ test }}', { name: 'test.njk' })).to.be.an('string'); }); describe('templates', function() { it('should return *NIX path seperators', function() { var fileName; precompile('./tests/templates/item.njk', { wrapper: function(templates) { fileName = templates[0].name; } }); expect(fileName).to.equal('./tests/templates/item.njk'); }); it('should return *NIX path seperators, when name is passed as option', function() { var fileName; precompile('test', { name: 'path\\to\\file.j2', isString: true, wrapper: function(templates) { fileName = templates[0].name; } }); expect(fileName).to.equal('path/to/file.j2'); }); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/runtime.js000664 000000 000000 00000006361 14012546311 023244 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect, util, finish, render; if (typeof require !== 'undefined') { expect = require('expect.js'); util = require('./util'); } else { expect = window.expect; util = window.util; } finish = util.finish; render = util.render; describe('runtime', function() { it('should report the failed function calls to symbols', function(done) { render('{{ foo("cvan") }}', {}, { noThrow: true }, function(err) { expect(err).to.match(/Unable to call `foo`, which is undefined/); }); finish(done); }); it('should report the failed function calls to lookups', function(done) { render('{{ foo["bar"]("cvan") }}', {}, { noThrow: true }, function(err) { expect(err).to.match(/foo\["bar"\]/); }); finish(done); }); it('should report the failed function calls to calls', function(done) { render('{{ foo.bar("second call") }}', {}, { noThrow: true }, function(err) { expect(err).to.match(/foo\["bar"\]/); }); finish(done); }); it('should report full function name in error', function(done) { render('{{ foo.barThatIsLongerThanTen() }}', {}, { noThrow: true }, function(err) { expect(err).to.match(/foo\["barThatIsLongerThanTen"\]/); }); finish(done); }); it('should report the failed function calls w/multiple args', function(done) { render('{{ foo.bar("multiple", "args") }}', {}, { noThrow: true }, function(err) { expect(err).to.match(/foo\["bar"\]/); }); render('{{ foo["bar"]["zip"]("multiple", "args") }}', {}, { noThrow: true }, function(err) { expect(err).to.match(/foo\["bar"\]\["zip"\]/); }); finish(done); }); it('should allow for undefined macro arguments in the last position', function(done) { render('{% macro foo(bar, baz) %}' + '{{ bar }} {{ baz }}{% endmacro %}' + '{{ foo("hello", nosuchvar) }}', {}, { noThrow: true }, function(err, res) { expect(err).to.equal(null); expect(typeof res).to.be('string'); }); finish(done); }); it('should allow for objects without a prototype macro arguments in the last position', function(done) { var noProto = Object.create(null); noProto.qux = 'world'; render('{% macro foo(bar, baz) %}' + '{{ bar }} {{ baz.qux }}{% endmacro %}' + '{{ foo("hello", noProto) }}', { noProto: noProto }, { noThrow: true }, function(err, res) { expect(err).to.equal(null); expect(res).to.equal('hello world'); }); finish(done); }); it('should not read variables property from Object.prototype', function(done) { var payload = 'function(){ return 1+2; }()'; var data = {}; Object.getPrototypeOf(data).payload = payload; render('{{ payload }}', data, { noThrow: true }, function(err, res) { expect(err).to.equal(null); expect(res).to.equal(payload); }); delete Object.getPrototypeOf(data).payload; finish(done); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/setup.js000664 000000 000000 00000000040 14012546311 022705 0ustar00rootroot000000 000000 process.env.NODE_ENV = 'test'; nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/000775 000000 000000 00000000000 14012546311 023213 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/async.njk000664 000000 000000 00000000031 14012546311 025026 0ustar00rootroot000000 000000 {{ tmpl | getContents }} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base-inherit.njk000664 000000 000000 00000000112 14012546311 026263 0ustar00rootroot000000 000000 {% extends "base.njk" %} {% block block1 %}*{{ super() }}*{% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base-set-and-show.njk000664 000000 000000 00000000126 14012546311 027137 0ustar00rootroot000000 000000 {% set var = 'parent' %}{% block main %}{% set var = 'inner' %}{% endblock %}{{ var }}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base-set-inside-block.njk000664 000000 000000 00000000065 14012546311 027764 0ustar00rootroot000000 000000 {% block main %}{% set var = 'inner' %}{% endblock %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base-set-wraps-block.njk000664 000000 000000 00000000114 14012546311 027640 0ustar00rootroot000000 000000 {% set somevar %}{% block somevar %}{% endblock %}{% endset %}{{ somevar }} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base-set.njk000664 000000 000000 00000000115 14012546311 025417 0ustar00rootroot000000 000000 {% set var = 'parent' %}{% block main %}{% set var = 'inner' %}{% endblock %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base-show.njk000664 000000 000000 00000000047 14012546311 025610 0ustar00rootroot000000 000000 {% block main %}{{ var }}{% endblock %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base.njk000664 000000 000000 00000000117 14012546311 024630 0ustar00rootroot000000 000000 Foo{% block block1 %}Bar{% endblock %}{% block block2 %}Baz{% endblock %}Fizzlenunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base2.njk000664 000000 000000 00000000113 14012546311 024706 0ustar00rootroot000000 000000 {% for item in [1,2] %}{% block item %}{{ item }}{% endblock %}{% endfor %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/base3.njk000664 000000 000000 00000000052 14012546311 024711 0ustar00rootroot000000 000000 {% block block1 %}Foo{% endblock %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/broken-conditional-include.njk000664 000000 000000 00000000101 14012546311 031111 0ustar00rootroot000000 000000 {% if not whatever %} {% include "throws.njk" %} {% endif %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/broken-import.njk000664 000000 000000 00000000115 14012546311 026504 0ustar00rootroot000000 000000 {% import 'doesnotexist' as doesnotexist %} str = {{ str | undefinedfilter }}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/filter-block.html000664 000000 000000 00000000155 14012546311 026457 0ustar00rootroot000000 000000 may the {% filter replace("force", "forth") %}{% block block1 %}bar{% endblock %}{% endfilter %} be with you nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/for-async-content.njk000664 000000 000000 00000000017 14012546311 027266 0ustar00rootroot000000 000000 somecontentherenunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/import-context-set.njk000664 000000 000000 00000000152 14012546311 027502 0ustar00rootroot000000 000000 {% set bar = "FOO" %} {# create a new scope #} {% for i in [1] %} {% set buzz = "buzz" %} {% endfor %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/import-context.njk000664 000000 000000 00000000057 14012546311 026715 0ustar00rootroot000000 000000 {% macro foo() %}Here's {{ bar }}{% endmacro %}tests/templates/import-macro-call-undefined-macro.njk000664 000000 000000 00000000156 14012546311 032222 0ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a{% import 'macro-call-undefined-macro.njk' as t %} {% for el in list %} {{ t.defined_macro() }} {% endfor %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/import.njk000664 000000 000000 00000000214 14012546311 025226 0ustar00rootroot000000 000000 {% macro foo() %}Here's a macro{% endmacro %} {% set bar = 'baz' %} {% macro wrap(el) %}<{{ el }}>{{ caller() }}{% endmacro %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/include-in-loop.njk000664 000000 000000 00000000064 14012546311 026715 0ustar00rootroot000000 000000 {{ loop.index }},{{ loop.index0 }},{{ loop.first }} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/include-set.njk000664 000000 000000 00000000044 14012546311 026131 0ustar00rootroot000000 000000 {{ var }}{% set var = 2 %}{{ var }} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/include.njk000664 000000 000000 00000000025 14012546311 025337 0ustar00rootroot000000 000000 FooInclude {{ name }}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/includeMany.njk000664 000000 000000 00000007070 14012546311 026173 0ustar00rootroot000000 000000 {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} {% include "include.njk" %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/item.njk000664 000000 000000 00000000022 14012546311 024647 0ustar00rootroot000000 000000 showing {{ item }}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/macro-call-undefined-macro.njk000664 000000 000000 00000000127 14012546311 030767 0ustar00rootroot000000 000000 {% macro defined_macro(useless) %} {% include "undefined-macro.njk" %} {% endmacro %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/000775 000000 000000 00000000000 14012546311 025026 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/dir1/000775 000000 000000 00000000000 14012546311 025665 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/dir1/index.njk000664 000000 000000 00000000034 14012546311 027475 0ustar00rootroot000000 000000 {% include "./macros.njk" %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/dir1/macros.njk000664 000000 000000 00000000005 14012546311 027650 0ustar00rootroot000000 000000 Test1nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/dir2/000775 000000 000000 00000000000 14012546311 025666 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/dir2/index.njk000664 000000 000000 00000000034 14012546311 027476 0ustar00rootroot000000 000000 {% include "./macros.njk" %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/dir2/macros.njk000664 000000 000000 00000000005 14012546311 027651 0ustar00rootroot000000 000000 Test2nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/test-cache.njk000664 000000 000000 00000000101 14012546311 027542 0ustar00rootroot000000 000000 {% include "./dir1/index.njk" %} {% include "./dir2/index.njk" %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/test1.njk000664 000000 000000 00000000103 14012546311 026564 0ustar00rootroot000000 000000 {% extends "../base.njk" %} {% block block1 %}Test1{% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/relative/test2.njk000664 000000 000000 00000000103 14012546311 026565 0ustar00rootroot000000 000000 {% extends "./test1.njk" %} {% block block1 %}Test2{% endblock %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/set.njk000664 000000 000000 00000000030 14012546311 024503 0ustar00rootroot000000 000000 {% set foo = "mumble" %}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/simple-base.njk000664 000000 000000 00000000044 14012546311 026116 0ustar00rootroot000000 000000 {% block test %}{% endblock test %} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/throws.njk000664 000000 000000 00000000026 14012546311 025243 0ustar00rootroot000000 000000 {{ nonExistentFn() }} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/templates/undefined-macro.njk000664 000000 000000 00000000016 14012546311 026754 0ustar00rootroot000000 000000 {{ undef() }} nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/test-node-pkgs/000775 000000 000000 00000000000 14012546311 024061 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/test-node-pkgs/dummy-pkg/000775 000000 000000 00000000000 14012546311 025773 5ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/test-node-pkgs/dummy-pkg/index.js000664 000000 000000 00000000000 14012546311 027426 0ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/test-node-pkgs/dummy-pkg/package.json000664 000000 000000 00000000444 14012546311 030263 0ustar00rootroot000000 000000 { "name": "dummy-pkg", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "Frankie Dintino (http://www.frankiedintino.com/)", "license": "ISC" } tests/test-node-pkgs/dummy-pkg/simple-template.html000664 000000 000000 00000000011 14012546311 031674 0ustar00rootroot000000 000000 nunjucks-fd500902d7c88672470c87170796de52fc0f791a{{ foo }}nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/tests.js000664 000000 000000 00000021505 14012546311 022720 0ustar00rootroot000000 000000 (function() { 'use strict'; var expect, util, render, equal; if (typeof require !== 'undefined') { expect = require('expect.js'); util = require('./util'); } else { expect = window.expect; util = window.util; } render = util.render; equal = util.equal; describe('tests', function() { it('callable should detect callability', function() { var callable = render('{{ foo is callable }}', { foo: function() { return '!!!'; } }); var uncallable = render('{{ foo is not callable }}', { foo: '!!!' }); expect(callable).to.be('true'); expect(uncallable).to.be('true'); }); it('defined should detect definedness', function() { expect(render('{{ foo is defined }}')).to.be('false'); expect(render('{{ foo is not defined }}')).to.be('true'); expect(render('{{ foo is defined }}', { foo: null })).to.be('true'); expect(render('{{ foo is not defined }}', { foo: null })).to.be('false'); }); it('should support "is defined" in {% if %} expressions', function() { expect( render('{% if foo is defined %}defined{% else %}undefined{% endif %}', {}) ).to.be('undefined'); expect( render('{% if foo is defined %}defined{% else %}undefined{% endif %}', {foo: null}) ).to.be('defined'); }); it('should support "is not defined" in {% if %} expressions', function() { expect( render('{% if foo is not defined %}undefined{% else %}defined{% endif %}', {}) ).to.be('undefined'); expect( render('{% if foo is not defined %}undefined{% else %}defined{% endif %}', {foo: null}) ).to.be('defined'); }); it('undefined should detect undefinedness', function() { expect(render('{{ foo is undefined }}')).to.be('true'); expect(render('{{ foo is not undefined }}')).to.be('false'); expect(render('{{ foo is undefined }}', { foo: null })).to.be('false'); expect(render('{{ foo is not undefined }}', { foo: null })).to.be('true'); }); it('none/null should detect strictly null values', function() { // required a change in lexer.js @ 220 expect(render('{{ null is null }}')).to.be('true'); expect(render('{{ none is none }}')).to.be('true'); expect(render('{{ none is null }}')).to.be('true'); expect(render('{{ foo is null }}')).to.be('false'); expect(render('{{ foo is not null }}', { foo: null })).to.be('false'); }); it('divisibleby should detect divisibility', function() { var divisible = render('{{ "6" is divisibleby(3) }}'); var notDivisible = render('{{ 3 is not divisibleby(2) }}'); expect(divisible).to.be('true'); expect(notDivisible).to.be('true'); }); it('escaped should test whether or not something is escaped', function() { var escaped = render('{{ (foo | safe) is escaped }}', { foo: 'foobarbaz' }); var notEscaped = render('{{ foo is escaped }}', { foo: 'foobarbaz' }); expect(escaped).to.be('true'); expect(notEscaped).to.be('false'); }); it('even should detect whether or not a number is even', function() { var fiveEven = render('{{ "5" is even }}'); var fourNotEven = render('{{ 4 is not even }}'); expect(fiveEven).to.be('false'); expect(fourNotEven).to.be('false'); }); it('odd should detect whether or not a number is odd', function() { var fiveOdd = render('{{ "5" is odd }}'); var fourNotOdd = render('{{ 4 is not odd }}'); expect(fiveOdd).to.be('true'); expect(fourNotOdd).to.be('true'); }); it('mapping should detect Maps or hashes', function() { /* global Map */ var map1, map2, mapOneIsMapping, mapTwoIsMapping; if (typeof Map === 'undefined') { this.skip(); } else { map1 = new Map(); map2 = {}; mapOneIsMapping = render('{{ map is mapping }}', { map: map1 }); mapTwoIsMapping = render('{{ map is mapping }}', { map: map2 }); expect(mapOneIsMapping).to.be('true'); expect(mapTwoIsMapping).to.be('true'); } }); it('falsy should detect whether or not a value is falsy', function() { var zero = render('{{ 0 is falsy }}'); var pancakes = render('{{ "pancakes" is not falsy }}'); expect(zero).to.be('true'); expect(pancakes).to.be('true'); }); it('truthy should detect whether or not a value is truthy', function() { var nullTruthy = render('{{ null is truthy }}'); var pancakesNotTruthy = render('{{ "pancakes" is not truthy }}'); expect(nullTruthy).to.be('false'); expect(pancakesNotTruthy).to.be('false'); }); it('greaterthan than should detect whether or not a value is less than another', function() { var fiveGreaterThanFour = render('{{ "5" is greaterthan(4) }}'); var fourNotGreaterThanTwo = render('{{ 4 is not greaterthan(2) }}'); expect(fiveGreaterThanFour).to.be('true'); expect(fourNotGreaterThanTwo).to.be('false'); }); it('ge should detect whether or not a value is greater than or equal to another', function() { var fiveGreaterThanEqualToFive = render('{{ "5" is ge(5) }}'); var fourNotGreaterThanEqualToTwo = render('{{ 4 is not ge(2) }}'); expect(fiveGreaterThanEqualToFive).to.be('true'); expect(fourNotGreaterThanEqualToTwo).to.be('false'); }); it('lessthan than should detect whether or not a value is less than another', function() { var fiveLessThanFour = render('{{ "5" is lessthan(4) }}'); var fourNotLessThanTwo = render('{{ 4 is not lessthan(2) }}'); expect(fiveLessThanFour).to.be('false'); expect(fourNotLessThanTwo).to.be('true'); }); it('le should detect whether or not a value is less than or equal to another', function() { var fiveLessThanEqualToFive = render('{{ "5" is le(5) }}'); var fourNotLessThanEqualToTwo = render('{{ 4 is not le(2) }}'); expect(fiveLessThanEqualToFive).to.be('true'); expect(fourNotLessThanEqualToTwo).to.be('true'); }); it('ne should detect whether or not a value is not equal to another', function() { var five = render('{{ 5 is ne(5) }}'); var four = render('{{ 4 is not ne(2) }}'); expect(five).to.be('false'); expect(four).to.be('false'); }); it('iterable should detect that a generator is iterable', function(done) { /* eslint-disable no-eval */ var iterable; try { iterable = eval('(function* iterable() { yield true; })()'); } catch (e) { return this.skip(); // Browser does not support generators } equal('{{ fn is iterable }}', { fn: iterable }, 'true'); return done(); }); it('iterable should detect that an Array is not non-iterable', function() { equal('{{ arr is not iterable }}', { arr: [] }, 'false'); }); it('iterable should detect that a Map is iterable', function() { /* global Map */ if (typeof Map === 'undefined') { this.skip(); } else { equal('{{ map is iterable }}', { map: new Map() }, 'true'); } }); it('iterable should detect that a Set is not non-iterable', function() { /* global Set */ if (typeof Set === 'undefined') { this.skip(); } else { equal('{{ set is not iterable }}', { set: new Set() }, 'false'); } }); it('number should detect whether a value is numeric', function() { var num = render('{{ 5 is number }}'); var str = render('{{ "42" is number }}'); expect(num).to.be('true'); expect(str).to.be('false'); }); it('string should detect whether a value is a string', function() { var num = render('{{ 5 is string }}'); var str = render('{{ "42" is string }}'); expect(num).to.be('false'); expect(str).to.be('true'); }); it('equalto should detect value equality', function() { var same = render('{{ 1 is equalto(2) }}'); var notSame = render('{{ 2 is not equalto(2) }}'); expect(same).to.be('false'); expect(notSame).to.be('false'); }); it('sameas should alias to equalto', function() { var obj = {}; var same = render('{{ obj1 is sameas(obj2) }}', { obj1: obj, obj2: obj }); expect(same).to.be('true'); }); it('lower should detect whether or not a string is lowercased', function() { expect(render('{{ "foobar" is lower }}')).to.be('true'); expect(render('{{ "Foobar" is lower }}')).to.be('false'); }); it('upper should detect whether or not a string is uppercased', function() { expect(render('{{ "FOOBAR" is upper }}')).to.be('true'); expect(render('{{ "Foobar" is upper }}')).to.be('false'); }); }); }()); nunjucks-fd500902d7c88672470c87170796de52fc0f791a/tests/util.js000664 000000 000000 00000012006 14012546311 022527 0ustar00rootroot000000 000000 (function() { /* eslint-disable vars-on-top */ 'use strict'; var nunjucks, nunjucksFull, isSlim = false, Environment, Template, Loader, precompileString, templatesPath, expect; if (typeof window === 'undefined') { nunjucks = nunjucksFull = require('../nunjucks/index.js'); Loader = nunjucks.FileSystemLoader; templatesPath = 'tests/templates'; expect = require('expect.js'); } else { nunjucks = window.nunjucks; if (window.nunjucksFull) { isSlim = true; nunjucksFull = window.nunjucksFull; // These must be the same for instanceof checks to succeed nunjucksFull.runtime.SafeString.prototype = nunjucks.runtime.SafeString.prototype; } else { nunjucksFull = window.nunjucksFull = nunjucks; } Loader = nunjucksFull.WebLoader; templatesPath = '../templates'; expect = window.expect; } precompileString = nunjucksFull.precompileString; Environment = nunjucks.Environment; Template = nunjucks.Template; var numAsyncs; var doneHandler; beforeEach(function() { numAsyncs = 0; doneHandler = null; }); function equal(str, ctx, opts, str2, env) { if (typeof ctx === 'string') { env = opts; str2 = ctx; ctx = null; opts = {}; } if (typeof opts === 'string') { env = str2; str2 = opts; opts = {}; } opts = opts || {}; var res = render(str, ctx, opts, env); expect(res).to.be(str2); } function jinjaEqual(str, ctx, str2, env) { var jinjaUninstalls = [nunjucks.installJinjaCompat()]; if (nunjucksFull !== nunjucks) { jinjaUninstalls.push(nunjucksFull.installJinjaCompat()); } try { return equal(str, ctx, str2, env); } finally { for (var i = 0; i < jinjaUninstalls.length; i++) { jinjaUninstalls[i](); } } } function finish(done) { if (numAsyncs > 0) { doneHandler = done; } else { done(); } } function normEOL(str) { if (!str) { return str; } return str.replace(/\r\n|\r/g, '\n'); } function randomTemplateName() { var rand = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5); return rand + '.njk'; } // eslint-disable-next-line consistent-return function render(str, ctx, opts, env, cb) { if (typeof ctx === 'function') { cb = ctx; ctx = null; opts = null; env = null; } else if (typeof opts === 'function') { cb = opts; opts = null; env = null; } else if (typeof env === 'function') { cb = env; env = null; } opts = opts || {}; opts.dev = true; var loader; var e; if (isSlim) { e = env || new Environment([], opts); loader = e.loaders[0]; } else { loader = new Loader(templatesPath); e = env || new Environment(loader, opts); } var name; if (opts.filters) { for (name in opts.filters) { if (Object.prototype.hasOwnProperty.call(opts.filters, name)) { e.addFilter(name, opts.filters[name]); } } } if (opts.asyncFilters) { for (name in opts.asyncFilters) { if (Object.prototype.hasOwnProperty.call(opts.asyncFilters, name)) { e.addFilter(name, opts.asyncFilters[name], true); } } } if (opts.extensions) { for (name in opts.extensions) { if (Object.prototype.hasOwnProperty.call(opts.extensions, name)) { e.addExtension(name, opts.extensions[name]); } } } var tmplName; if (isSlim) { tmplName = randomTemplateName(); var precompileJs = precompileString(str, { name: tmplName, asFunction: true, env: e }); eval(precompileJs); // eslint-disable-line no-eval } ctx = ctx || {}; var t; if (isSlim) { var tmplSource = loader.getSource(tmplName); t = new Template(tmplSource.src, e, tmplSource.path); } else { t = new Template(str, e); } if (!cb) { return t.render(ctx); } else { numAsyncs++; t.render(ctx, function(err, res) { if (err && !opts.noThrow) { throw err; } try { cb(err, normEOL(res)); } catch (exc) { if (doneHandler) { doneHandler(exc); numAsyncs = 0; doneHandler = null; } else { throw exc; } } numAsyncs--; if (numAsyncs === 0 && doneHandler) { doneHandler(); } }); } } if (typeof window === 'undefined') { module.exports.render = render; module.exports.equal = equal; module.exports.jinjaEqual = jinjaEqual; module.exports.finish = finish; module.exports.normEOL = normEOL; module.exports.isSlim = isSlim; module.exports.Loader = Loader; } else { window.util = { render: render, equal: equal, jinjaEqual: jinjaEqual, finish: finish, normEOL: normEOL, isSlim: isSlim, Loader: Loader, }; } }());