Compare commits

...

117 Commits

Author SHA1 Message Date
Jakob Linskeseder ca0d385a6c Coerce noEscape-statements to a string
Also removed `chai-diff`-dependency, since
it is unmaintained and not that useful.

Fixes #1838
2023-08-29 23:45:05 +02:00
Jakob Linskeseder 520e1d5f08 Fix links in README
This commit backports changes from `master`.
2023-08-02 23:37:05 +02:00
Jakob Linskeseder 02423780a9 Fix link to homepage
When visiting the www-variant, a `SSL_ERROR_INTERNAL_ERROR_ALERT`
error is displayed by the browser.
2023-08-02 23:35:42 +02:00
Jakob Linskeseder be92d2f254 Backport: publish latest and tags to AWS S3
In order to automate publishing 4.x releases to AWS S3,
we also need this workflow on the 4.x branch.

See #1972
2023-08-02 23:09:34 +02:00
Jakob Linskeseder 443a613b3a Add guideline for pushing to RubyGems 2023-08-02 22:56:15 +02:00
Nils Knappmeier 83ee5908f2 add links to publish requirements 2023-08-01 23:24:13 +02:00
Jakob Linskeseder 8dc3d2517b v4.7.8 2023-08-01 23:06:18 +02:00
Jakob Linskeseder 668c4fb878 Fix browser tests in CI pipeline 2023-07-23 20:46:46 +02:00
Jakob Linskeseder c65c6cce3f Test on Node 18
* Replaced outdated `grunt-bg-shell`-package to get rid of
  coffee-script warnings

Backports parts of 785a63e0a8.
2022-10-21 16:48:14 +02:00
Marnix Kok 3d3796c1e9 Make library compatible with workers
When using Handlebars in a Cloudflare Worker, an environment
in which the `window` and `global` objects both don't exist,
an error is thrown about `window` being undefined.

According to the ECMA specification, only `self` is available
in a worker. Since we support old runtimes in our 4.x branch,
we can't use `globalThis` but have to use a polyfill.
2022-10-16 22:17:50 +02:00
Jakob Linskeseder 075b354a3b Fix sync issue with npm lock-file
```
npm ERR! `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm ERR!
npm ERR! Invalid: lock file's acorn@5.7.4 does not satisfy acorn@7.4.1
npm ERR! Missing: acorn@5.7.4 from lock file
```
2022-10-16 21:55:05 +02:00
arturmakaranka 30dbf04781 Fix compiling of each block params in strict mode
Fixes #1820
Closes #1821
2022-05-25 22:18:36 +02:00
Jakob Linskeseder e3a54485db Fix bundler issue with webpack 5
As explained in issue #1844 and in issue
https://github.com/webpack/webpack/issues/15007#issuecomment-996615250,
the way we used the `browser`-field was wrong.

The main reason for using the `browser`-field is the requirement of
`require('fs')` in the main-entry-file.
The workaround for this was using `require('handlebars/lib/handlebars')`,
but now it will also work via `require('handlebars')` for bundlers that
respect the `browser`-field.

The `"./runtime"`-config was removed, because it didn't have any effect.
In order to detect regressions, the webpack-integration test was
extended to test with different webpack versions.

Fixes #1174
Closes #1844
2022-05-17 21:19:48 +02:00
Jakob Linskeseder 8e23642ea2 Fix integration-tests issue with npm >= 7
Fixes the following error when running integration-tests:
```
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: webpack-test@1.0.0
npm ERR! Found: handlebars@5.0.0-alpha.1
npm ERR! node_modules/handlebars
npm ERR!   dev handlebars@"file:../../.." from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer handlebars@">= 1.3.0 < 5" from handlebars-loader@1.7.1
npm ERR! node_modules/handlebars-loader
npm ERR!   dev handlebars-loader@"^1.7.1" from the root project
npm ERR!
```
2022-05-17 21:19:48 +02:00
James Wilson 88ac06875f use https instead of git for mustache submodule 2022-03-19 19:19:27 +01:00
Jakob Linskeseder c68bc08a0d Fix typo 2022-01-08 00:39:29 +01:00
Nils Knappmeier 6cfbc2653a Use eslint-env "shared-node-browser" for shared handlebars code 2022-01-07 18:15:48 +01:00
Jakob Linskeseder b65135acef Replace shields.io with faster badgen.net
Badgen is a faster successor of shields.io:
https://github.com/badgen/badgen.net

Backport from `master`-branch.
2021-12-30 00:58:48 +01:00
Jakob Linskeseder e2f63da5c0 Don't create security alerts for dev-dependencies 2021-12-29 22:48:01 +01:00
Jakob Linskeseder 78e7e28ff9 Replace custom test-server with Grunt connect 2021-12-29 21:35:16 +01:00
Jakob Linskeseder 03d387bf8e Fix rollup warning when importing Handlebars as ESM
Closes #1696
2021-12-29 21:35:16 +01:00
Jakob Linskeseder e0f50b4eec Add integration-tests to CI workflow 2021-12-29 21:35:16 +01:00
Jakob Linskeseder 9ed9418488 Replace Saucelabs with Playwright
Also reorganized npm scripts.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder ef0fc290b9 Move print-script.js-file in tests-folder
Also removed it from published files, since the script doesn't seem to be used.

Fourth part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder edc65b5c19 Move integration-testing-folder in tests-folder
Third part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder 715f4af179 Remove redundant task-part in task-tests-folder
Second part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder 3bd0fa8b32 Move bench-folder in tests-folder
First part of reorganizing and cleaning up test-folders.
2021-12-29 21:35:16 +01:00
Jakob Linskeseder c295ef085f Update repository URL 2021-12-03 23:12:20 +01:00
Jakob Linskeseder c1ad3c8057 Update contributing guidelines 2021-12-03 23:12:20 +01:00
Jakob Linskeseder af92e32822 Run CI tests on more versions 2021-12-03 23:12:20 +01:00
Jakob Linskeseder 2954e7ea66 Don't rely on Node.js global object
If `global` is used and handlebars is compiled for browser
usage without a Node.js `global` polyfill, handlebars
fails with a `global is undefined` error.

Fixes #1593
2021-12-03 21:57:55 +01:00
Jay Linski 8eefee56ff Replace Travis CI with GitHub action (#1772) 2021-12-03 19:04:01 +01:00
Shon Frazier fd93073146 Fix Mustache links
...to point to 'https://mustache.github.io/...'
2021-11-23 23:58:57 +01:00
Nils Knappmeier a9a8e40321 v4.7.7 2021-02-15 10:36:07 +01:00
Nils Knappmeier e66aed5b99 Update release notes 2021-02-15 10:35:33 +01:00
Nils Knappmeier 7d4d170ce4 disable IE in Saucelabs tests
The SauceLabs IE setup has problems and does not work
with the endpoints that we use to run them. This is a Saucelabs problem, but it has been there for a year now. Until we use another API, I will simply remove the browsers
2021-02-15 09:59:41 +01:00
Nils Knappmeier eb860c0899 fix weird error in integration tests
somehow the variable "$i" gets overwritten in integration-testing/multi-nodejs-test/test.sh:20, which is why we now rename it to a more meaningful variable
2021-02-15 09:50:16 +01:00
Nils Knappmeier b6d3de7123 fix: check prototype property access in strict-mode (#1736) 2021-02-13 13:41:40 +01:00
Nils Knappmeier f058970169 fix: escape property names in compat mode (#1736) 2021-02-13 13:40:53 +01:00
John Boehr 77825f8d35 refator: In spec tests, use expectTemplate over equals and shouldThrow (#1683) 2020-05-05 00:04:33 +02:00
Hannah Wolfe 3789a30955 chore: start testing on Node.js 12 and 13
- make sure we're aware of all compatibility issues
2020-04-03 20:05:19 +01:00
Hannah Wolfe e6ad93ea01 v4.7.6 2020-04-03 18:56:22 +01:00
Hannah Wolfe 2bf4fc6fd3 Update release notes 2020-04-03 18:55:39 +01:00
Hannah Wolfe b64202bc91 Update release-notes.md
- indicate changes have been reverted
2020-04-03 18:47:56 +01:00
Douglas Christopher Wilson c2f1e62031 Switch cmd parser to latest minimist 2020-04-03 18:44:30 +01:00
Hannah Wolfe 08e9a11a34 Revert "chore: set Node.js compatibility to v6+"
This reverts commit 309d2b49a1.
2020-04-03 18:44:30 +01:00
Hannah Wolfe 1fd2edee2a v4.7.5 2020-04-02 20:09:49 +01:00
Hannah Wolfe 3c9c2f5cf2 Update release notes 2020-04-02 20:09:16 +01:00
Hannah Wolfe 16487a088e chore: downgrade yargs to v14
- allows us to support Node v6 and v8 for now
2020-04-02 19:28:36 +01:00
Hannah Wolfe 309d2b49a1 chore: set Node.js compatibility to v6+
- drop support for versions less than Node v6 via engines
- update integration tests to also test newer versions
2020-04-02 19:28:35 +01:00
Nils Knappmeier 645ac73844 test: fix integration tests
- They should fail, if any test fails
- Use "set -e" instead of "|| exit 1", because it suffices to be specified
  at the top of each file
2020-04-02 19:25:37 +01:00
Nils Knappmeier b454b02147 docs: update release-docs in CONTRIBUTING.md
- remove ember testing
- add docs for updating the website.
- add list of required accesses to publish
2020-04-02 14:10:47 +02:00
Hannah Wolfe 7adc19ab40 v4.7.4 2020-04-01 18:20:30 +01:00
Hannah Wolfe 9dd8d10e12 Update release notes 2020-04-01 18:19:34 +01:00
Hannah Wolfe 4671c4b383 Use tmp directory for files written during tests
- Add spec/tmp directory with .gitkeep file to indicate the folder is intentional
- Add the folder contents to .gitignore
- Use this folder to output the sourcemap file during bin tests. This file is a sideeffect of the main test
2020-04-01 18:04:14 +01:00
DeeDeeG e46baa1fe2 tasks/test-bin.js: Delete duplicate test
Per comment by @ErisDS on the GitHub PR:
https://github.com/wycats/handlebars.js/pull/1666#issuecomment-606805248
2020-04-01 17:25:35 +01:00
Hannah Wolfe c491b4ea25 Revert "Update release-notes.md"
This reverts commit 738391a064.
Think this needs to be left to the release tool :|
2020-04-01 17:21:20 +01:00
Hannah Wolfe 738391a064 Update release-notes.md 2020-03-31 17:05:33 +01:00
Aori Nevo 80c4516fda chore: add unit tests for cli options (#1666)
- adds full unit tests for all cli options
- demonstrates that nothing changes between minimist and yargs except a minor order change in the --help text
- proves b9c4b253e works the same as before

Co-authored-by: fabb <fabb@users.noreply.github.com>
2020-03-31 16:54:11 +01:00
Avi Vahl d79212a662 fix: migrate from optimist to yargs (#1666)
closes #1658

- adapted code from master to latest yargs (`.option` calls).

```
4.x:
found 188 vulnerabilities (169 low, 4 moderate, 14 high, 1 critical) in 5815 scanned packages

4.x with this PR:
found 32 vulnerabilities (17 low, 1 moderate, 13 high, 1 critical) in 5829 scanned packages
```
2020-03-31 16:54:11 +01:00
Avi Vahl b440c38886 chore: ignore external @types in tests
- some indirect dependencies install @types packages which are not compatible with the older typescript.
- adjusted test's tsconfig to not pick these up automatically, as the actual .d.ts does not depend on these external types.
2020-03-31 16:53:54 +01:00
Eli Skeggs 2dba7eee3f docs: fix comparison link 2020-03-05 15:10:10 +01:00
Nils Knappmeier c9789691af v4.7.3 2020-02-05 06:10:33 +01:00
Nils Knappmeier 9278f217e0 Update release notes 2020-02-05 06:10:09 +01:00
roydukkey d78cc73d3c Fixes spelling and punctuation
(cherry picked from commit fd3ca85d13)
2020-02-05 06:06:42 +01:00
ismailjones 4de51fe26b Add Type Definition for Handlebars.VERSION, Fixes #1647 2020-02-04 23:32:10 +01:00
papasmile a32d05f2fc Include Type Definition for runtime.js in Package 2020-02-04 21:58:49 +01:00
Nils Knappmeier ad63f5189f chore: add missing "await" in aws-s3 publishing code
closes #1644
2020-01-21 21:52:23 +01:00
Nils Knappmeier 586e672c8b v4.7.2 2020-01-13 21:53:14 +01:00
Nils Knappmeier f0c6c4cc1f Update release notes 2020-01-13 21:52:50 +01:00
Nils Knappmeier a4fd391ba1 chore: execute saucelabs-task only if access-key exists
- up to now, the existance of the SAUCE_USERNAME was checked
  but this variable is even present in pull-requests from other
  repos. This means that builds fail, because the access key
  is not there.
  This change looks for SAUCE_ACCESS_KEY instead, which is
  a secure variable, only present in build originating from
  the handlebars.js repo.
2020-01-13 21:47:51 +01:00
Nils Knappmeier 9d5aa363cf fix: don't wrap helpers that are not functions
- helpers should always be a function, but in #1639 one seems to
  be undefined. This was not a problem before 4.6 because helpers
  weren't wrapped then.
  Now, we must take care only to wrap helpers (when adding
  the "lookupProperty" function to the options), if they
  are really functions.
2020-01-13 21:39:01 +01:00
Nils Knappmeier 14ba3d0c43 v4.7.1 2020-01-12 13:21:08 +01:00
Nils Knappmeier 4cddfe7017 Update release notes 2020-01-12 13:20:37 +01:00
Nils Knappmeier f152dfc892 fix: fix log output in case of illegal property access
- fix link url to handlebarsjs.com
2020-01-12 13:09:19 +01:00
Nils Knappmeier 3c1e252169 fix: log error for illegal property access only once per property 2020-01-12 13:06:56 +01:00
Nils Knappmeier 0d5c807017 v4.7.0 2020-01-10 17:24:06 +01:00
Nils Knappmeier 1f0834b1a2 Update release notes 2020-01-10 17:23:31 +01:00
Nils Knappmeier 575d8772e2 fix: use "logger" instead of console.error
... to be graceful with older browser without "console"
2020-01-10 17:06:57 +01:00
Nils Knappmeier 7af1c12db6 feat: default options for controlling proto access
This commmit adds the runtime options
- `allowProtoPropertiesByDefault` (boolean, default: false) and
- `allowProtoMethodsByDefault` (boolean, default: false)`
which can be used to allow access to prototype properties and
functions in general.

Specific properties and methods can still be disabled from access
via `allowedProtoProperties` and `allowedProtoMethods` by
setting the corresponding values to false.

The methods `constructor`, `__defineGetter__`, `__defineSetter__`, `__lookupGetter__`
and the property `__proto__` will be disabled, even if the allow...ByDefault-options
are set to true. In order to allow access to those properties and methods, they have
to be explicitly set to true in the 'allowedProto...'-options.

A warning is logged when the a proto-access it attempted and denied
by default (i.e. if no option is set by the user to make the access
decision explicit)
2020-01-10 16:55:45 +01:00
Nils Knappmeier 91a1b5d2f4 v4.6.0 2020-01-08 23:45:15 +01:00
Nils Knappmeier 770d746e60 Update release notes 2020-01-08 23:44:46 +01:00
Nils Knappmeier d7f0dcf2bb refactor: fix typo in private test method 2020-01-08 23:17:23 +01:00
Nils Knappmeier 187d611e8c test: add path to nodeJs when running test:bin
- this allows the test to be run in a debugger
  without the complete PATH
2020-01-08 23:17:23 +01:00
Nils Knappmeier d337f40d0e test: show diff when test:bin fails 2020-01-08 23:17:23 +01:00
Nils Knappmeier d03b6ecfc4 feat: access control to prototype properties via whitelist
Disallow access to prototype properties and methods by default.
Access to properties is always checked via
`Object.prototype.hasOwnProperty.call(parent, propertyName)`.

New runtime options:
- **allowedProtoMethods**: a string-to-boolean map of property-names that are allowed if they are methods of the parent object.
- **allowedProtoProperties**: a string-to-boolean map of property-names that are allowed if they are properties but not methods of the parent object.

```js
const template = handlebars.compile('{{aString.trim}}')
const result = template({ aString: '  abc  ' })
// result is empty, because trim is defined at String prototype
```

```js
const template = handlebars.compile('{{aString.trim}}')
const result = template({ aString: '  abc  ' }, {
  allowedProtoMethods: {
    trim: true
  }
})
// result = 'abc'
```

Implementation details: The method now "container.lookupProperty"
handles the prototype-checks and the white-lists. It is used in
- JavaScriptCompiler#nameLookup
- The "lookup"-helper (passed to all helpers as "options.lookupProperty")
- The "lookup" function at the container, which is used for recursive lookups in "compat" mode

Compatibility:
- **Old precompiled templates work with new runtimes**: The "options.lookupPropery"-function is passed to the helper by a wrapper, not by the compiled templated.
- **New templates work with old runtimes**: The template contains a function that is used as fallback if the "lookupProperty"-function cannot be found at the container. However, the runtime-options "allowedProtoProperties" and "allowedProtoMethods" only work with the newest runtime.

BREAKING CHANGE:
- access to prototype properties is forbidden completely by default
2020-01-08 23:17:23 +01:00
Nils Knappmeier 164b7ff0de chore: ignore .nyc_output 2019-12-14 18:41:06 +01:00
Nils Knappmeier ac4655ee95 chore: disable "dot-notation" rule
- I don't think it makes much sense. In some cases,
  it is more readable to wrap property access in quotes,
  some times not, but there is no universal rule
  for that.
- a promises "catch"-function should not be wrapped, but it has to
  be, if "allow-keywords" is set to false
- an "if"-helper should be wrapped, but it is not allowed to be
  if "allow-keywords" is set to true (default).
2019-12-14 18:41:06 +01:00
Nils Knappmeier 14b621caf5 test/style: remove or hide unused code in git.js, add tests 2019-12-14 18:41:06 +01:00
Nils Knappmeier 1ec1737d24 test/style: refactor remaining grunt tasks to use promises instead of callbacks 2019-12-14 18:41:06 +01:00
Nils Knappmeier 1ebce2b53c test/style: use nyc instead of istanbul, npm audit fix 2019-12-14 18:41:06 +01:00
Nils Knappmeier 3a5b65e02b test/style: refactor parser task 2019-12-14 18:41:06 +01:00
Nils Knappmeier dde108e283 test/style: refactor test-task to make it more readable 2019-12-14 18:41:06 +01:00
Nils Knappmeier dc5495216d chore: change eslint-rules for tasks/
- use es2017 rules as NodeJS supports it today
- add "prefer-const"
2019-12-14 18:41:06 +01:00
Marcos Marado d1fb07b32b Update (C) year in the LICENSE file
Welcome to 2019!
2019-12-08 16:21:38 +01:00
Nils Knappmeier 04b19848cc chore: try to fix saucelabs credentials (#1627) 2019-12-08 16:17:15 +01:00
Nils Knappmeier c40d9f33ad chore: active linting and formatting on commit 2019-12-05 08:01:33 +01:00
Nils Knappmeier 8901c28e9a chore: fix task name in build 2019-12-03 22:59:04 +01:00
Nils Knappmeier e97685e989 style: reformat all files using prettier 2019-12-03 22:37:15 +01:00
Nils Knappmeier e913dc5f12 chore: restructure build commands
- move dtslint (checkTypes) away from Gruntfile.js
  (as it disturbs debugging `grunt`)
  and call it directly as npm-script in travis-ci
- re-group task definition in Gruntfile.js
- use a multi-job travis build to
  run linting, format, dtslint and tests
  in parallel
- run only "npm test" on appveyor
- rename Grunt-tasks for be more descriptive
- SAUCE_USERNAME is not a secret variable anymore
  it can be easily guessed anyway and the
  [secure] value messes up with a lot of the
  log output

- linting on commit disable during transition
2019-12-03 22:21:30 +01:00
Nils Knappmeier 1f61f21250 chore: configure prettier and eslint
- add prettier to do formatting
- add eslint-config-prettier to
  disable rules conflicting with prettier
- remove eslint from grunt workflow
- use lint-stage to lint and format
  on precommit
- use eslint and prettier in travis directly
- remove rules that are already part of
  the "recommended" ruleset

That rational is that eslint and prettier should be run in
Travis-CI, on commit and as IDE integration (highlighting
errors directlry). They don't need to be run along with
test-cases. Getting linting errors when running the tests
because of missing semicolons is just annoying, but doesn't
help the overall code-quality.
2019-12-02 22:40:05 +01:00
Nils Knappmeier 587e7a3e63 remove yarn.lock 2019-12-02 22:40:04 +01:00
ole-martin edcc84f292 Update readme.md with updated links (#1620)
* Update readme.md with updated links

Fix http->https where possible and update links doc links

* Update README.markdown

* Fix precompilation link
2019-12-02 22:03:52 +01:00
ole-martin 23d58e79bb fix(runtime.js): partials compile not caching (#1600)
Reintroduce "merge" function, no called "mergeIfNeeded", that only creates a new partials
object if both "env.partials" and "options.partials" are set.

closes #1598
2019-11-18 21:21:03 +01:00
Nils Knappmeier c819c8b533 v4.5.3 2019-11-18 08:10:26 +01:00
Nils Knappmeier 827c9d0747 Update release notes 2019-11-18 08:09:48 +01:00
Nils Knappmeier f7f05d7558 fix: add "no-prototype-builtins" eslint-rule and fix all occurences 2019-11-18 07:33:45 +01:00
Nils Knappmeier 1988878087 fix: add more properties required to be enumerable
- __defineGetter__, __defineSetter__, __lookupGetter__, __proto__
2019-11-18 07:33:45 +01:00
Nils Knappmeier 886ba86c2f test/chore: add chai/expect and sinon to "runtime"-environment 2019-11-17 22:18:20 +01:00
Nils Knappmeier 0817dad7e7 test: add sinon as global variable to eslint in the specs 2019-11-15 17:22:29 +01:00
Nils Knappmeier 93516a0b07 test: add sinon.js for spies, deprecate current assertions
- the goal is to get overall cleaner test.
- chai and sinon are standard libraries for testing
  assertions and spies. We should not use custom ones.
2019-11-15 16:38:28 +01:00
Nils Knappmeier 93e284ed9b chore: add chai and dirty-chai for better test assertions 2019-11-15 16:38:28 +01:00
Nils Knappmeier c02b05fa81 fix: use !== 0 instead of != 0 2019-11-15 16:38:28 +01:00
Nils Knappmeier 8de121d21c v4.5.2 2019-11-13 22:07:27 +01:00
Nils Knappmeier 6914090086 Update release notes 2019-11-13 22:06:51 +01:00
Nils Knappmeier d54137810a fix: use String(field) in lookup when checking for "constructor"
closes #1603
2019-11-13 22:02:05 +01:00
Nils Knappmeier c2ac79c970 test: add fluent API for testing Handlebars
use "expectTemplate(template)....toCompileTo(output)"
2019-11-13 22:02:05 +01:00
222 changed files with 30790 additions and 12996 deletions
+14
View File
@@ -0,0 +1,14 @@
root = true
[*.js]
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[spec/expected/*.js]
insert_final_newline = false
[*.yml]
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
+24
View File
@@ -0,0 +1,24 @@
.rvmrc
.DS_Store
/tmp/
*.sublime-project
*.sublime-workspace
npm-debug.log
.idea
yarn-error.log
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
/spec/vendor
# JS-Snippets
src/*.js
+51 -102
View File
@@ -1,117 +1,66 @@
module.exports = {
"extends": ["eslint:recommended","plugin:compat/recommended"],
"globals": {
"self": false
extends: ['eslint:recommended', 'plugin:compat/recommended', 'prettier'],
globals: {
self: false
},
"env": {
"node": true,
"es6": true
env: {
node: true,
es6: true
},
"rules": {
// overrides eslint:recommended defaults
"no-sparse-arrays": "off",
"no-func-assign": "off",
"no-console": "warn",
"no-debugger": "warn",
"no-unreachable": "warn",
// Possible Errors //
//-----------------//
"no-unsafe-negation": "error",
rules: {
'no-console': 'warn',
// temporarily disabled until the violating places are fixed.
'no-func-assign': 'off',
'no-sparse-arrays': 'off',
// Best Practices //
//----------------//
"curly": "error",
"default-case": "warn",
"dot-notation": ["error", { "allowKeywords": false }],
"guard-for-in": "warn",
"no-alert": "error",
"no-caller": "error",
"no-div-regex": "warn",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-floating-decimal": "error",
"no-implied-eval": "error",
"no-iterator": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-loop-func": "error",
"no-multi-spaces": "error",
"no-multi-str": "warn",
"no-global-assign": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-process-env": "error",
"no-proto": "error",
"no-return-assign": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unused-expressions": "error",
"no-warning-comments": "warn",
"no-with": "error",
"radix": "error",
"wrap-iife": "error",
'default-case': 'warn',
'guard-for-in': 'warn',
'no-alert': 'error',
'no-caller': 'error',
'no-div-regex': 'warn',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-floating-decimal': 'error',
'no-implied-eval': 'error',
'no-iterator': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-loop-func': 'error',
'no-multi-str': 'warn',
'no-global-assign': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-process-env': 'error',
'no-proto': 'error',
'no-return-assign': 'error',
'no-script-url': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-unused-expressions': 'error',
'no-warning-comments': 'warn',
'no-with': 'error',
radix: 'error',
// Variables //
//-----------//
"no-catch-shadow": "error",
"no-label-var": "error",
"no-shadow-restricted-names": "error",
"no-undef-init": "error",
"no-use-before-define": ["error", "nofunc"],
// Stylistic Issues //
//------------------//
"comma-dangle": ["error", "never"],
"quote-props": ["error", "as-needed", { "keywords": true, "unnecessary": false }],
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"camelcase": "error",
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": ["error", "last"],
"consistent-this": ["warn", "self"],
"eol-last": "error",
"func-style": ["error", "declaration"],
"key-spacing": ["error", {
"beforeColon": false,
"afterColon": true
}],
"new-cap": "error",
"new-parens": "error",
"no-array-constructor": "error",
"no-lonely-if": "error",
"no-mixed-spaces-and-tabs": "error",
"no-nested-ternary": "warn",
"no-new-object": "error",
"no-spaced-func": "error",
"no-trailing-spaces": "error",
"no-extra-parens": ["error", "functions"],
"quotes": ["error", "single", "avoid-escape"],
"semi": "error",
"semi-spacing": ["error", { "before": false, "after": true }],
"keyword-spacing": "error",
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", { "anonymous": "never", "named": "never" }],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": ["error", "always", { "markers": [","] }],
"wrap-regex": "warn",
'no-label-var': 'error',
'no-undef-init': 'error',
'no-use-before-define': ['error', 'nofunc'],
// ECMAScript 6 //
//--------------//
"no-var": "warn"
'no-var': 'error'
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 6,
"ecmaFeatures": {}
parserOptions: {
sourceType: 'module',
ecmaVersion: 6,
ecmaFeatures: {}
}
}
};
+9
View File
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
open-pull-requests-limit: 0
schedule:
interval: weekly
allow:
- dependency-type: production
+89
View File
@@ -0,0 +1,89 @@
name: CI
on:
push:
branches:
- master
pull_request: {}
jobs:
lint:
name: Lint
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
test:
name: Test (Node)
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/
node-version: ['10', '12', '14', '16', '18', '20']
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Test
run: npm run test
- name: Test (Integration)
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version != '18' && matrix.node-version != '20' }}
run: |
cd ./tests/integration/rollup-test && ./test.sh && cd -
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
cd ./tests/integration/webpack-test && ./test.sh && cd -
browser:
name: Test (Browser)
runs-on: 'ubuntu-20.04'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: |
npx playwright install-deps
npx playwright install
- name: Build
run: npx grunt prepare
- name: Test
run: npm run test:browser
+38
View File
@@ -0,0 +1,38 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- master
tags:
- '*'
jobs:
publish-aws-s3:
name: Publish to AWS S3
runs-on: 'ubuntu-latest'
environment: 'builds.handlebarsjs.com.s3.amazonaws.com'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Publish
run: |
git config --global user.email "release@handlebarsjs.com"
git config --global user.name "handlebars-lang"
npm run publish:aws
env:
S3_BUCKET_NAME: "builds.handlebarsjs.com"
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+11 -7
View File
@@ -1,15 +1,19 @@
vendor
.rvmrc
.DS_Store
lib/handlebars/compiler/parser.js
/dist/
/tmp/
/coverage/
node_modules
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
.idea
yarn-error.log
/yarn-error.log
/yarn.lock
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/integration/*/dist/
/spec/tmp/*
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "spec/mustache"]
path = spec/mustache
url = git://github.com/mustache/spec.git
url = https://github.com/mustache/spec.git
-2
View File
@@ -1,2 +0,0 @@
instrumentation:
excludes: ['**/spec/**', '**/handlebars/compiler/parser.js']
+22
View File
@@ -0,0 +1,22 @@
.rvmrc
.DS_Store
/tmp/
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
.idea
yarn-error.log
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
/spec/vendor
-26
View File
@@ -1,26 +0,0 @@
language: node_js
before_install:
- npm install -g grunt-cli
script:
- grunt --stack travis
email:
on_failure: change
on_success: never
env:
global:
- S3_BUCKET_NAME=builds.handlebarsjs.com
- secure: ckyEe5dzjdFDjmZ6wIrhGm0CFBEnKq8c1dYptfgVV/Q5/nJFGzu8T0yTjouS/ERxzdT2H327/63VCxhFnLCRHrsh4rlW/rCy4XI3O/0TeMLgFPa4TXkO8359qZ4CB44TBb3NsJyQXNMYdJpPLTCVTMpuiqqkFFOr+6OeggR7ufA=
- secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8=
matrix:
include:
- node_js: '10'
env:
- PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
cache:
directories:
- node_modules
git:
depth: 100
+123 -37
View File
@@ -2,18 +2,18 @@
## Reporting Issues
Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Please see our [FAQ](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]! In general we are going to ask for an example of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle [template][jsfiddle] to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here)
Pull requests containing only failing tests demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
Documentation issues on the handlebarsjs.com site should be reported on [handlebars-site](https://github.com/wycats/handlebars-site).
Documentation issues on the [handlebarsjs.com](https://handlebarsjs.com) site should be reported on [handlebars-lang/docs](https://github.com/handlebars-lang/docs).
## Branches
* The branch `4.x` contains the currently released version. Bugfixes should be made in this branch.
* The branch `master` contains the next version. A release date is not yet specified. Maintainers
- The branch `4.x` contains the currently released version. Bugfixes should be made in this branch.
- The branch `master` contains the next version. A release date is not yet specified. Maintainers
should merge the branch `4.x` into the master branch regularly.
## Pull Requests
@@ -21,6 +21,7 @@ Documentation issues on the handlebarsjs.com site should be reported on [handleb
We also accept [pull requests][pull-request]!
Generally we like to see pull requests that
- Maintain the existing code style
- Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request)
- Have [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
@@ -31,8 +32,8 @@ Generally we like to see pull requests that
To build Handlebars.js you'll need a few things installed.
* Node.js
* [Grunt](http://gruntjs.com/getting-started)
- Node.js
- [Grunt](http://gruntjs.com/getting-started)
Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`.
@@ -46,16 +47,18 @@ You can also run our set of benchmarks with `grunt bench`.
The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
[http://github.com/handlebars-lang/handlebars.js/issues](http://github.com/handlebars-lang/handlebars.js/issues).
##Running Tests
## Running Tests
To run tests locally, first install all dependencies.
```sh
npm install
```
Clone the mustache specs into the spec/mustache folder.
```sh
cd spec
rm -r mustache
@@ -63,54 +66,137 @@ git clone https://github.com/mustache/spec.git mustache
```
From the root directory, run the tests.
```sh
npm test
```
## Ember testing
## Linting and Formatting
The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script.
Handlebars uses `eslint` to enforce best-practices and `prettier` to auto-format files.
We do linting and formatting in two phases:
```sh
npm link
grunt build release
cp dist/*.js $emberRepoDir/bower_components/handlebars/
- Committed files are linted and formatted in a pre-commit hook. In this stage eslint-errors are forbidden,
while warnings are allowed.
- The GitHub CI job also lints all files and checks if they are formatted correctly. In this stage, warnings
are forbidden.
cd $emberRepoDir
npm link handlebars
npm test
```
You can use the following scripts to make sure that the CI job does not fail:
- **npm run lint** will run `eslint` and fail on warnings
- **npm run format** will run `prettier` on all files
- **npm run check-before-pull-request** will perform all most checks that our CI job does in its build-job, excluding the "integration-test".
- **npm run test:integration** will run integration tests (using old NodeJS versions and integrations with webpack, babel and so on)
These tests only work on a Linux-machine with `nvm` installed (for running tests in multiple versions of NodeJS).
## Releasing the latest version
*When releasing a previous version of Handlebars, please look into the CONTRIBUNG.md in the corresponding branch.*
Before attempting the release Handlebars, please make sure that you have the following authorizations:
Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
- Push-access to [handlebars-lang/handlebars.js](https://github.com/handlebars-lang/handlebars.js/)
- Publishing rights on npmjs.com for the [handlebars](https://www.npmjs.com/package/handlebars) package
- Publishing rights on rubygems for the [handlebars-source](https://rubygems.org/gems/handlebars-source) package
- Push-access to the repo for legacy package managers: [components/handlebars.js](https://github.com/components/handlebars.js)
- Push-access to the production-repo of the handlebars site: [handlebars-lang/docs](https://github.com/handlebars-lang/docs)
A full release may be completed with the following:
_When releasing a previous version of Handlebars, please look into the CONTRIBUNG.md in the corresponding branch._
```
npm ci
yo release
npm publish
yo release:publish components handlebars.js dist/components/
A full release via Docker may be completed with the following:
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
1. Create a `Dockerfile` in this folder for releasing
```Dockerfile
FROM node:10-slim
ENV EDITOR=vim
# Update stretch repositories
RUN sed -i -e 's/deb.debian.org/archive.debian.org/g' \
-e 's|security.debian.org|archive.debian.org/|g' \
-e '/stretch-updates/d' /etc/apt/sources.list
# Install release dependencies
RUN apt-get update
RUN apt-get install -y git vim
# Work around deprecated npm dependency install via unauthenticated git-protocol:
# https://github.com/kpdecker/generator-release/blob/87aab9b84c9f083635c3fcc822f18acce1f48736/package.json#L31
RUN git config --system url."https://github.com/".insteadOf git://github.com/
# Configure git
RUN git config --system user.email "release@handlebarsjs.com"
RUN git config --system user.name "handlebars-lang"
RUN mkdir /home/node/.config
RUN mkdir /home/node/.ssh
RUN mkdir /home/node/tmp
# Generate config for yo generator-release:
# https://github.com/kpdecker/generator-release#example
# You have to add a valid GitHub OAuth token!
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub OAuth token'\n};" > /home/node/.config/generator-release
RUN chown -R node:node /home/node/.config
# Add the generated key to GitHub: https://github.com/settings/keys
RUN ssh-keygen -q -t ed25519 -N '' -f /home/node/.ssh/id_ed25519 -C "release@handlebarsjs.com"
RUN chmod 0600 /home/node/.ssh/id_ed25519*
RUN chown node:node /home/node/.ssh/id_ed25519*
```
2. Build and run the Docker image
```bash
docker build --tag handlebars:release .
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
--user $(id -u):$(id -g) \
--env NPM_CONFIG_PREFIX=/home/node/.npm-global \
handlebars:release bash -c 'export PATH=$PATH:/home/node/.npm-global/bin; bash'
```
* Add SSH key to GitHub: `cat /home/node/.ssh/id_ed25519.pub` (https://github.com/settings/keys)
* Add GitHub API token: `vi /home/node/.config/generator-release`
* Execute the following steps:
```bash
npm ci
npm install -g yo@1 grunt@1 generator-release
npm run release
yo release
npm login
npm publish
yo release:publish components handlebars.js dist/components/
```
6. Publish Ruby `handlebars-source` gem:
```bash
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
--user $(id -u):$(id -g) \
ruby:3.2-slim bash
```
* Execute the following steps:
```bash
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
### After the release
After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
in those places still point to the latest version
* [The npm-package](https://www.npmjs.com/package/handlebars) (check latest-tag)
* [The bower package](https://github.com/components/handlebars.js) (check the package.json)
* [The AWS S3 Bucket](https://s3.amazonaws.com/builds.handlebarsjs.com) (check latest-tag)
* [RubyGems](https://rubygems.org/gems/handlebars-source)
- [The npm-package](https://www.npmjs.com/package/handlebars) (check latest-tag)
- [The bower package](https://github.com/components/handlebars.js) (check the package.json)
- [The AWS S3 Bucket](https://s3.amazonaws.com/builds.handlebarsjs.com) (check latest-tag)
- [RubyGems](https://rubygems.org/gems/handlebars-source)
When everything is OK, the handlebars site needs to be updated to point to the new version numbers. The jsfiddle link should be updated to point to the most recent distribution for all instances in our documentation.
When everything is OK, the **handlebars site** needs to be updated.
Go to the master branch of the repo [handlebars-lang/docs](https://github.com/handlebars-lang/docs/tree/master)
and make a minimal change to the README. This will invoke a github-action that redeploys
the site, fetching the latest version-number from the npm-registry.
(note that the default-branch of this repo is not the master and regular changes are done
in the `handlebars-lang/docs`-repo).
[generator-release]: https://github.com/walmartlabs/generator-release
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[issue]: https://github.com/wycats/handlebars.js/issues/new
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
[issue]: https://github.com/handlebars-lang/handlebars.js/issues/new
[jsfiddle]: https://jsfiddle.net/9D88g/180/
+7 -5
View File
@@ -2,11 +2,11 @@
1. How can I file a bug report:
See our guidelines on [reporting issues](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
See our guidelines on [reporting issues](https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
1. Why isn't my Mustache template working?
Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/wycats/handlebars.js#differences-between-handlebarsjs-and-mustache).
Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/handlebars-lang/handlebars.js#differences-between-handlebarsjs-and-mustache).
1. Why is it slower when compiling?
@@ -36,16 +36,18 @@
```sh
handlebars --version
```
If using the integrated precompiler and
```javascript
console.log(Handlebars.VERSION);
```
On the client side.
We include the built client libraries in the npm package for those who want to be certain that they are using the same client libraries as the compiler.
Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
1. Why doesn't IE like the `default` name in the AMD module?
@@ -53,8 +55,8 @@
1. How do I load the runtime library when using AMD?
There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/wycats/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field.
There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/handlebars-lang/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field.
The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases.
+88 -106
View File
@@ -1,45 +1,42 @@
/* eslint-disable no-process-env */
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
eslint: {
files: [
'*.js',
'bench/**/*.js',
'tasks/**/*.js',
'lib/**/!(*.min|parser).js',
'spec/**/!(*.amd|json2|require).js',
'integration-testing/multi-nodejs-test/*.js',
'integration-testing/webpack-test/*.js',
'integration-testing/webpack-test/src/*.js'
]
},
clean: ['tmp', 'dist', 'lib/handlebars/compiler/parser.js', 'integration-testing/**/node_modules'],
clean: [
'tmp',
'dist',
'lib/handlebars/compiler/parser.js',
'/tests/integration/**/node_modules'
],
copy: {
dist: {
options: {
processContent: function(content) {
return grunt.template.process('/**!\n\n @license\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n*/\n')
+ content;
return (
grunt.template.process(
'/**!\n\n @license\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n*/\n'
) + content
);
}
},
files: [
{expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/'}
]
files: [{ expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/' }]
},
cdnjs: {
files: [
{expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/cdnjs'}
{ expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/cdnjs' }
]
},
components: {
files: [
{expand: true, cwd: 'components/', src: ['**'], dest: 'dist/components'},
{expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/components'}
{
expand: true,
cwd: 'components/',
src: ['**'],
dest: 'dist/components'
},
{ expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/components' }
]
}
},
@@ -54,24 +51,28 @@ module.exports = function(grunt) {
options: {
modules: 'amd'
},
files: [{
expand: true,
cwd: 'lib/',
src: '**/!(index).js',
dest: 'dist/amd/'
}]
files: [
{
expand: true,
cwd: 'lib/',
src: '**/!(index).js',
dest: 'dist/amd/'
}
]
},
cjs: {
options: {
modules: 'common'
},
files: [{
cwd: 'lib/',
expand: true,
src: '**/!(index).js',
dest: 'dist/cjs/'
}]
files: [
{
cwd: 'lib/',
expand: true,
src: '**/!(index).js',
dest: 'dist/cjs/'
}
]
}
},
webpack: {
@@ -80,7 +81,12 @@ module.exports = function(grunt) {
module: {
loaders: [
// the optional 'runtime' transformer tells babel to require the runtime instead of inlining it.
{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules&auxiliaryCommentBefore=istanbul%20ignore%20next' }
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader:
'babel-loader?optional=runtime&loose=es6.modules&auxiliaryCommentBefore=istanbul%20ignore%20next'
}
]
},
output: {
@@ -129,15 +135,17 @@ module.exports = function(grunt) {
preserveComments: /(?:^!|@(?:license|preserve|cc_on))/
},
dist: {
files: [{
cwd: 'dist/',
expand: true,
src: ['handlebars*.js', '!*.min.js'],
dest: 'dist/',
rename: function(dest, src) {
return dest + src.replace(/\.js$/, '.min.js');
files: [
{
cwd: 'dist/',
expand: true,
src: ['handlebars*.js', '!*.min.js'],
dest: 'dist/',
rename: function(dest, src) {
return dest + src.replace(/\.js$/, '.min.js');
}
}
}]
]
}
},
@@ -157,48 +165,11 @@ module.exports = function(grunt) {
}
}
},
'saucelabs-mocha': {
all: {
options: {
build: process.env.TRAVIS_JOB_ID,
urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'],
detailedError: true,
concurrency: 4,
browsers: [
{browserName: 'chrome'},
{browserName: 'firefox', platform: 'Linux'},
// {browserName: 'safari', version: 9, platform: 'OS X 10.11'},
// {browserName: 'safari', version: 8, platform: 'OS X 10.10'},
{browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'},
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'}
]
}
},
sanity: {
options: {
build: process.env.TRAVIS_JOB_ID,
urls: ['http://localhost:9999/spec/umd.html?headless=true', 'http://localhost:9999/spec/amd-runtime.html?headless=true', 'http://localhost:9999/spec/umd-runtime.html?headless=true'],
detailedError: true,
concurrency: 2,
browsers: [
{browserName: 'chrome'}
]
}
}
},
bgShell: {
checkTypes: {
cmd: 'npm run checkTypes',
bg: false,
fail: true
},
shell: {
integrationTests: {
cmd: './integration-testing/run-integration-tests.sh',
bg: false,
fail: true
command: './tests/integration/run-integration-tests.sh'
}
},
watch: {
@@ -208,26 +179,11 @@ module.exports = function(grunt) {
},
files: ['src/*', 'lib/**/*.js', 'spec/**/*.js'],
tasks: ['build', 'amd', 'tests', 'test']
tasks: ['on-file-change']
}
}
});
// Build a new version of the library
this.registerTask('build', 'Builds a distributable version of the current project', [
'eslint',
'bgShell:checkTypes',
'parser',
'node',
'globals']);
this.registerTask('amd', ['babel:amd', 'requirejs']);
this.registerTask('node', ['babel:cjs']);
this.registerTask('globals', ['webpack']);
this.registerTask('tests', ['concat:tests']);
this.registerTask('release', 'Build final packages', ['eslint', 'amd', 'uglify', 'test:min', 'copy:dist', 'copy:components', 'copy:cdnjs']);
// Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
@@ -237,19 +193,45 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-bg-shell');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('@knappi/grunt-saucelabs');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
grunt.registerTask('bench', ['metrics']);
grunt.registerTask('sauce', process.env.SAUCE_USERNAME ? ['tests', 'connect', 'saucelabs-mocha'] : []);
grunt.registerTask('node', ['babel:cjs']);
grunt.registerTask('amd', ['babel:amd', 'requirejs']);
grunt.registerTask('globals', ['webpack']);
grunt.registerTask('release', 'Build final packages', [
'uglify',
'test:min',
'copy:dist',
'copy:components',
'copy:cdnjs'
]);
grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'bgShell:integrationTests', 'sauce', 'metrics', 'publish:latest'] : ['default']);
// Requires secret properties from .travis.yaml
grunt.registerTask('extensive-tests-and-publish-to-aws', [
'default',
'shell:integrationTests',
'metrics',
'publish-to-aws'
]);
grunt.registerTask('on-file-change', ['build', 'concat:tests', 'test']);
// === Primary tasks ===
grunt.registerTask('dev', ['clean', 'connect', 'watch']);
grunt.registerTask('default', ['clean', 'build', 'test', 'release']);
grunt.registerTask('integration-tests', ['default', 'bgShell:integrationTests']);
grunt.registerTask('test', ['test:bin', 'test:cov']);
grunt.registerTask('bench', ['metrics']);
grunt.registerTask('prepare', ['build', 'concat:tests']);
grunt.registerTask(
'build',
'Builds a distributable version of the current project',
['parser', 'node', 'amd', 'globals']
);
grunt.registerTask('integration-tests', [
'default',
'shell:integrationTests'
]);
};
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (C) 2011-2017 by Yehuda Katz
Copyright (C) 2011-2019 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+23 -24
View File
@@ -1,28 +1,29 @@
[![Travis Build Status](https://img.shields.io/travis/wycats/handlebars.js/master.svg)](https://travis-ci.org/wycats/handlebars.js)
[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/github/wycats/handlebars.js?branch=master&svg=true)](https://ci.appveyor.com/project/wycats/handlebars-js)
[![Selenium Test Status](https://saucelabs.com/buildstatus/handlebars)](https://saucelabs.com/u/handlebars)
[![CI Build Status](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml/badge.svg)](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/handlebars/badge?style=rounded)](https://www.jsdelivr.com/package/npm/handlebars)
[![npm downloads](https://badgen.net/npm/dm/handlebars)](https://www.npmjs.com/package/handlebars)
[![npm version](https://badgen.net/npm/v/handlebars)](https://www.npmjs.com/package/handlebars)
[![Bundle size](https://badgen.net/bundlephobia/minzip/handlebars?label=minified%20%2B%20gzipped)](https://bundlephobia.com/package/handlebars)
[![Install size](https://packagephobia.com/badge?p=handlebars)](https://packagephobia.com/result?p=handlebars)
Handlebars.js
=============
Handlebars.js is an extension to the [Mustache templating
language](http://mustache.github.com/) created by Chris Wanstrath.
Handlebars.js and Mustache are both logicless templating languages that
keep the view and the code separated like we all know they should be.
Handlebars provides the power necessary to let you build **semantic templates** effectively with no frustration.
Handlebars is largely compatible with Mustache templates. In most cases it is possible to swap out Mustache with Handlebars and continue using your current templates.
Checkout the official Handlebars docs site at
[http://www.handlebarsjs.com](http://www.handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/).
[handlebarsjs.com](https://handlebarsjs.com) and try our [live demo](https://handlebarsjs.com/playground.html).
Installing
----------
See our [installation documentation](http://handlebarsjs.com/installation.html).
See our [installation documentation](https://handlebarsjs.com/installation/).
Usage
-----
In general, the syntax of Handlebars.js templates is a superset
of Mustache templates. For basic syntax, check out the [Mustache
manpage](http://mustache.github.com/mustache.5.html).
manpage](https://mustache.github.io/mustache.5.html).
Once you have a template, use the `Handlebars.compile` method to compile
the template into a function. The generated function takes a context
@@ -46,25 +47,25 @@ var result = template(data);
// </ul>
```
Full documentation and more examples are at [handlebarsjs.com](http://handlebarsjs.com/).
Full documentation and more examples are at [handlebarsjs.com](https://handlebarsjs.com/).
Precompiling Templates
----------------------
Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](http://handlebarsjs.com/precompilation.html).
Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](https://handlebarsjs.com/installation/precompilation.html).
Differences Between Handlebars.js and Mustache
----------------------------------------------
Handlebars.js adds a couple of additional features to make writing
templates easier and also changes a tiny detail of how partials work.
- [Nested Paths](http://handlebarsjs.com/#paths)
- [Helpers](http://handlebarsjs.com/#helpers)
- [Block Expressions](http://handlebarsjs.com/#block-expressions)
- [Literal Values](http://handlebarsjs.com/#literals)
- [Delimited Comments](http://handlebarsjs.com/#comments)
- [Nested Paths](https://handlebarsjs.com/guide/expressions.html#path-expressions)
- [Helpers](https://handlebarsjs.com/guide/expressions.html#helpers)
- [Block Expressions](https://handlebarsjs.com/guide/block-helpers.html#basic-blocks)
- [Literal Values](https://handlebarsjs.com/guide/expressions.html#literal-segments)
- [Delimited Comments](https://handlebarsjs.com/guide/#template-comments)
Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](http://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](https://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
### Compatibility
@@ -89,8 +90,6 @@ Handlebars has been designed to work in any ECMAScript 3 environment. This inclu
Older versions and other runtimes are likely to work but have not been formally
tested. The compiler requires `JSON.stringify` to be implemented natively or via a polyfill. If using the precompiler this is not necessary.
[![Selenium Test Status](https://saucelabs.com/browser-matrix/handlebars.svg)](https://saucelabs.com/u/handlebars)
Performance
-----------
@@ -102,18 +101,18 @@ does have some big performance advantages. Justin Marney, a.k.a.
[gotascii](http://github.com/gotascii), confirmed that with an
[independent test](http://sorescode.com/2010/09/12/benchmarks.html). The
rewritten Handlebars (current version) is faster than the old version,
with many [performance tests](https://travis-ci.org/wycats/handlebars.js/builds/33392182#L538) being 5 to 7 times faster than the Mustache equivalent.
with many performance tests being 5 to 7 times faster than the Mustache equivalent.
Upgrading
---------
See [release-notes.md](https://github.com/wycats/handlebars.js/blob/master/release-notes.md) for upgrade notes.
See [release-notes.md](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) for upgrade notes.
Known Issues
------------
See [FAQ.md](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
See [FAQ.md](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
Handlebars in the Wild
@@ -165,4 +164,4 @@ License
-------
Handlebars.js is released under the MIT license.
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
-37
View File
@@ -1,37 +0,0 @@
# Test against these versions of Node.js
environment:
matrix:
- nodejs_version: "10"
platform:
- x64
# Install scripts (runs after repo cloning)
install:
# Get the latest stable version of Node.js
- ps: Install-Product node $env:nodejs_version $env:platform
# Clone submodules (mustache spec)
- cmd: git submodule update --init --recursive
# Install modules
- cmd: npm install
- cmd: npm install -g grunt-cli
# Post-install test scripts
test_script:
# Output useful info for debugging
- cmd: node --version
- cmd: npm --version
# Run tests
- cmd: grunt --stack travis
# Don't actually build
build: off
on_failure:
- cmd: 7z a coverage.zip coverage
- cmd: appveyor PushArtifact coverage.zip
# Set build version format here instead of in the admin panel
version: "{build}"
-14
View File
@@ -1,14 +0,0 @@
{
"globals": {
"require": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,
"no-var": 0,
"no-shadow": 0,
"handle-callback-err": 0,
"no-console": 0
}
}
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: '{{#names}}{{name}}{{/names}}'
};
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}'
};
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
context: { names: [{bat: 'foo', name: ['Moe']}, {bat: 'foo', name: ['Larry']}, {bat: 'foo', name: ['Curly']}, {bat: 'foo', name: ['Shemp']}], foo: 'bar' },
handlebars: '{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}',
mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}',
eco: '<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>'
};
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
context: { person: { name: {bar: {baz: 'Larry'}}, age: 45 } },
handlebars: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
dust: '{person.name.bar.baz}{person.age}{person.foo}{animal.age}',
eco: '<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>',
mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}'
};
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
rules: {
'no-console': 0,
'no-var': 0
}
};
+159 -111
View File
@@ -1,128 +1,176 @@
#!/usr/bin/env node
var optimist = require('optimist')
.usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...', {
'f': {
'type': 'string',
'description': 'Output File',
'alias': 'output'
},
'map': {
'type': 'string',
'description': 'Source Map File'
},
'a': {
'type': 'boolean',
'description': 'Exports amd style (require.js)',
'alias': 'amd'
},
'c': {
'type': 'string',
'description': 'Exports CommonJS style, path to Handlebars module',
'alias': 'commonjs',
'default': null
},
'h': {
'type': 'string',
'description': 'Path to handlebar.js (only valid for amd-style)',
'alias': 'handlebarPath',
'default': ''
},
'k': {
'type': 'string',
'description': 'Known helpers',
'alias': 'known'
},
'o': {
'type': 'boolean',
'description': 'Known helpers only',
'alias': 'knownOnly'
},
'm': {
'type': 'boolean',
'description': 'Minimize output',
'alias': 'min'
},
'n': {
'type': 'string',
'description': 'Template namespace',
'alias': 'namespace',
'default': 'Handlebars.templates'
},
's': {
'type': 'boolean',
'description': 'Output template function only.',
'alias': 'simple'
},
'N': {
'type': 'string',
'description': 'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.',
'alias': 'name'
},
'i': {
'type': 'string',
'description': 'Generates a template from the passed CLI argument.\n"-" is treated as a special value and causes stdin to be read for the template value.',
'alias': 'string'
},
'r': {
'type': 'string',
'description': 'Template root. Base value that will be stripped from template names.',
'alias': 'root'
},
'p': {
'type': 'boolean',
'description': 'Compiling a partial template',
'alias': 'partial'
},
'd': {
'type': 'boolean',
'description': 'Include data when compiling',
'alias': 'data'
},
'e': {
'type': 'string',
'description': 'Template extension.',
'alias': 'extension',
'default': 'handlebars'
},
'b': {
'type': 'boolean',
'description': 'Removes the BOM (Byte Order Mark) from the beginning of the templates.',
'alias': 'bom'
},
'v': {
'type': 'boolean',
'description': 'Prints the current compiler version',
'alias': 'version'
},
var argv = parseArgs({
'f': {
'type': 'string',
'description': 'Output File',
'alias': 'output'
},
'map': {
'type': 'string',
'description': 'Source Map File'
},
'a': {
'type': 'boolean',
'description': 'Exports amd style (require.js)',
'alias': 'amd'
},
'c': {
'type': 'string',
'description': 'Exports CommonJS style, path to Handlebars module',
'alias': 'commonjs',
'default': null
},
'h': {
'type': 'string',
'description': 'Path to handlebar.js (only valid for amd-style)',
'alias': 'handlebarPath',
'default': ''
},
'k': {
'type': 'string',
'description': 'Known helpers',
'alias': 'known'
},
'o': {
'type': 'boolean',
'description': 'Known helpers only',
'alias': 'knownOnly'
},
'm': {
'type': 'boolean',
'description': 'Minimize output',
'alias': 'min'
},
'n': {
'type': 'string',
'description': 'Template namespace',
'alias': 'namespace',
'default': 'Handlebars.templates'
},
's': {
'type': 'boolean',
'description': 'Output template function only.',
'alias': 'simple'
},
'N': {
'type': 'string',
'description': 'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.',
'alias': 'name'
},
'i': {
'type': 'string',
'description': 'Generates a template from the passed CLI argument.\n"-" is treated as a special value and causes stdin to be read for the template value.',
'alias': 'string'
},
'r': {
'type': 'string',
'description': 'Template root. Base value that will be stripped from template names.',
'alias': 'root'
},
'p': {
'type': 'boolean',
'description': 'Compiling a partial template',
'alias': 'partial'
},
'd': {
'type': 'boolean',
'description': 'Include data when compiling',
'alias': 'data'
},
'e': {
'type': 'string',
'description': 'Template extension.',
'alias': 'extension',
'default': 'handlebars'
},
'b': {
'type': 'boolean',
'description': 'Removes the BOM (Byte Order Mark) from the beginning of the templates.',
'alias': 'bom'
},
'v': {
'type': 'boolean',
'description': 'Prints the current compiler version',
'alias': 'version'
},
'help': {
'type': 'boolean',
'description': 'Outputs this message'
}
});
'help': {
'type': 'boolean',
'description': 'Outputs this message'
}
})
.wrap(120)
.check(function(argv) {
if (argv.version) {
return;
}
});
var argv = optimist.argv;
argv.files = argv._;
delete argv._;
var Precompiler = require('../dist/cjs/precompiler');
Precompiler.loadTemplates(argv, function(err, opts) {
if (err) {
throw err;
}
if (opts.help || (!opts.templates.length && !opts.version)) {
optimist.showHelp();
printUsage(argv._spec, 120);
} else {
Precompiler.cli(opts);
}
});
function pad(n) {
var str = '';
while (str.length < n) {
str += ' ';
}
return str;
}
function parseArgs(spec) {
var opts = { alias: {}, boolean: [], default: {}, string: [] };
Object.keys(spec).forEach(function (arg) {
var opt = spec[arg];
opts[opt.type].push(arg);
if ('alias' in opt) opts.alias[arg] = opt.alias;
if ('default' in opt) opts.default[arg] = opt.default;
});
var argv = require('minimist')(process.argv.slice(2), opts);
argv._spec = spec;
return argv;
}
function printUsage(spec, wrap) {
var wordwrap = require('wordwrap');
console.log('Precompile handlebar templates.');
console.log('Usage: handlebars [template|directory]...');
var opts = [];
var width = 0;
Object.keys(spec).forEach(function (arg) {
var opt = spec[arg];
var name = (arg.length === 1 ? '-' : '--') + arg;
if ('alias' in opt) name += ', --' + opt.alias;
var meta = '[' + opt.type + ']';
if ('default' in opt) meta += ' [default: ' + JSON.stringify(opt.default) + ']';
opts.push({ name: name, desc: opt.description, meta: meta });
if (name.length > width) width = name.length;
});
console.log('Options:');
opts.forEach(function (opt) {
var desc = wordwrap(width + 4, wrap + 1)(opt.desc);
console.log(' %s%s%s%s%s',
opt.name,
pad(width - opt.name.length + 2),
desc.slice(width + 4),
pad(wrap - opt.meta.length - desc.split(/\n/).pop().length),
opt.meta
);
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.5.1",
"version": "4.7.8",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
+1 -1
View File
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
gem.date = Time.now.strftime("%Y-%m-%d")
gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.}
gem.summary = %q{Handlebars.js source code wrapper}
gem.homepage = "https://github.com/wycats/handlebars.js/"
gem.homepage = "https://github.com/handlebars-lang/handlebars.js/"
gem.version = package["version"].sub "-", "."
gem.license = "MIT"
+3 -3
View File
@@ -2,10 +2,10 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>4.5.1</version>
<version>4.7.8</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
<licenseUrl>https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/handlebars-lang/handlebars.js/</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Extension of the Mustache logicless template language</description>
<releaseNotes></releaseNotes>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.5.1",
"version": "4.7.8",
"license": "MIT",
"jspm": {
"main": "handlebars",
+1 -1
View File
@@ -16,4 +16,4 @@ Decorators are executed when the block program is instantiated and are passed `(
Decorators may set values on `props` or return a modified function that wraps `program` in particular behaviors. If the decorator returns nothing, then `program` is left unaltered.
The [inline partial](https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors.
The [inline partial](https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors.
@@ -1,12 +0,0 @@
module.exports = {
"extends": "eslint:recommended",
"globals": {
"self": false
},
"env": {
"node": true
},
"rules": {
'no-console': 'off'
}
}
@@ -1,12 +0,0 @@
import * as Handlebars from 'handlebars/runtime'
import hbs from 'handlebars-inline-precompile';
import {assertEquals} from "../../webpack-test/src/lib/assert";
Handlebars.registerHelper('loud', function(text) {
return text.toUpperCase();
});
const template = hbs`{{loud name}}`;
const output = template({name: 'yehuda'})
assertEquals(output, 'YEHUDA')
@@ -1,5 +0,0 @@
export function assertEquals(actual, expected) {
if (actual !== expected) {
throw new Error(`Expected "${actual}" to equal "${expected}"`);
}
}
@@ -1,32 +0,0 @@
const fs = require('fs');
const testFiles = fs.readdirSync('src');
const entryPoints = {};
testFiles
.filter(file => file.match(/-test.js$/))
.forEach(file => {
entryPoints[file] = `./src/${file}`;
});
module.exports = {
entry: entryPoints,
output: {
filename: '[name]',
path: __dirname + '/dist'
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { cacheDirectory: false },
}
}
]
},
optimization: {
minimize: false
}
};
@@ -1,21 +0,0 @@
{
"name": "webpack-test",
"description": "Various tests with Handlebars and Webpack",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"test": "node dist/main.js"
},
"private": true,
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"handlebars": "file:../..",
"handlebars-loader": "^1.7.1",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7"
}
}
@@ -1,6 +0,0 @@
import Handlebars from 'handlebars';
import {assertEquals} from './lib/assert';
const template = Handlebars.compile('Author: {{author}}');
assertEquals(template({author: 'Yehuda'}), 'Author: Yehuda');
@@ -1,8 +0,0 @@
import {assertEquals} from './lib/assert';
import testTemplate from './test-template.handlebars';
assertEquals(testTemplate({author: 'Yehuda'}).trim(), 'Author: Yehuda');
const testTemplateRequire = require('./test-template.handlebars');
assertEquals(testTemplateRequire({author: 'Yehuda'}).trim(), 'Author: Yehuda');
@@ -1,5 +0,0 @@
import * as Handlebars from 'handlebars';
import {assertEquals} from './lib/assert';
const template = Handlebars.compile('Author: {{author}}');
assertEquals(template({author: 'Yehuda'}), 'Author: Yehuda');
@@ -1,5 +0,0 @@
export function assertEquals(actual, expected) {
if (actual !== expected) {
throw new Error(`Expected "${actual}" to equal "${expected}"`);
}
}
@@ -1,22 +0,0 @@
const fs = require('fs');
const testFiles = fs.readdirSync('src');
const entryPoints = {};
testFiles
.filter(file => file.match(/-test.js$/))
.forEach(file => {
entryPoints[file] = `./src/${file}`;
});
module.exports = {
entry: entryPoints,
output: {
filename: '[name]',
path: __dirname + '/dist'
},
module: {
rules: [
{test: /\.handlebars$/, loader: 'handlebars-loader'}
]
}
};
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
env: {
// Handlebars should not use node or browser-specific APIs
'shared-node-browser': true,
node: false,
browser: false
}
};
+5 -1
View File
@@ -2,7 +2,11 @@ import runtime from './handlebars.runtime';
// Compiler imports
import AST from './handlebars/compiler/ast';
import { parser as Parser, parse, parseWithoutProcessing } from './handlebars/compiler/base';
import {
parser as Parser,
parse,
parseWithoutProcessing
} from './handlebars/compiler/base';
import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
import Visitor from './handlebars/compiler/visitor';
+22 -8
View File
@@ -1,10 +1,11 @@
import {createFrame, extend, toString} from './utils';
import { createFrame, extend, toString } from './utils';
import Exception from './exception';
import {registerDefaultHelpers} from './helpers';
import {registerDefaultDecorators} from './decorators';
import { registerDefaultHelpers } from './helpers';
import { registerDefaultDecorators } from './decorators';
import logger from './logger';
import { resetLoggedProperties } from './internal/proto-access';
export const VERSION = '4.5.1';
export const VERSION = '4.7.8';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
@@ -38,7 +39,9 @@ HandlebarsEnvironment.prototype = {
registerHelper: function(name, fn) {
if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple helpers'); }
if (fn) {
throw new Exception('Arg not supported with multiple helpers');
}
extend(this.helpers, name);
} else {
this.helpers[name] = fn;
@@ -53,7 +56,9 @@ HandlebarsEnvironment.prototype = {
extend(this.partials, name);
} else {
if (typeof partial === 'undefined') {
throw new Exception(`Attempting to register a partial called "${name}" as undefined`);
throw new Exception(
`Attempting to register a partial called "${name}" as undefined`
);
}
this.partials[name] = partial;
}
@@ -64,7 +69,9 @@ HandlebarsEnvironment.prototype = {
registerDecorator: function(name, fn) {
if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple decorators'); }
if (fn) {
throw new Exception('Arg not supported with multiple decorators');
}
extend(this.decorators, name);
} else {
this.decorators[name] = fn;
@@ -72,9 +79,16 @@ HandlebarsEnvironment.prototype = {
},
unregisterDecorator: function(name) {
delete this.decorators[name];
},
/**
* Reset the memory of illegal property accesses that have already been logged.
* @deprecated should only be used in handlebars test-cases
*/
resetLoggedPropertyAccesses() {
resetLoggedProperties();
}
};
export let log = logger.log;
export {createFrame, logger};
export { createFrame, logger };
+10 -6
View File
@@ -5,24 +5,28 @@ let AST = {
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
helperExpression: function(node) {
return (node.type === 'SubExpression')
|| ((node.type === 'MustacheStatement' || node.type === 'BlockStatement')
&& !!((node.params && node.params.length) || node.hash));
return (
node.type === 'SubExpression' ||
((node.type === 'MustacheStatement' ||
node.type === 'BlockStatement') &&
!!((node.params && node.params.length) || node.hash))
);
},
scopedId: function(path) {
return (/^\.|this\b/).test(path.original);
return /^\.|this\b/.test(path.original);
},
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
simpleId: function(path) {
return path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth;
return (
path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth
);
}
}
};
// Must be exported as an object rather than the root of the module as the jison lexer
// must modify the object to operate properly.
export default AST;
+3 -1
View File
@@ -10,7 +10,9 @@ extend(yy, Helpers);
export function parseWithoutProcessing(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') { return input; }
if (input.type === 'Program') {
return input;
}
parser.yy = yy;
+28 -25
View File
@@ -1,12 +1,12 @@
/* global define */
import {isArray} from '../utils';
/* global define, require */
import { isArray } from '../utils';
let SourceNode;
try {
/* istanbul ignore next */
if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we asusme that
// We don't support this in AMD environments. For these environments, we assume that
// they are running on the browser and thus have no need for the source-map library.
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
@@ -38,7 +38,7 @@ if (!SourceNode) {
this.src = chunks + this.src;
},
toStringWithSourceMap: function() {
return {code: this.toString()};
return { code: this.toString() };
},
toString: function() {
return this.src;
@@ -46,7 +46,6 @@ if (!SourceNode) {
};
}
function castChunk(chunk, codeGen, loc) {
if (isArray(chunk)) {
let ret = [];
@@ -62,7 +61,6 @@ function castChunk(chunk, codeGen, loc) {
return chunk;
}
function CodeGen(srcFile) {
this.srcFile = srcFile;
this.source = [];
@@ -94,17 +92,22 @@ CodeGen.prototype = {
},
empty: function() {
let loc = this.currentLocation || {start: {}};
let loc = this.currentLocation || { start: {} };
return new SourceNode(loc.start.line, loc.start.column, this.srcFile);
},
wrap: function(chunk, loc = this.currentLocation || {start: {}}) {
wrap: function(chunk, loc = this.currentLocation || { start: {} }) {
if (chunk instanceof SourceNode) {
return chunk;
}
chunk = castChunk(chunk, this, loc);
return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk);
return new SourceNode(
loc.start.line,
loc.start.column,
this.srcFile,
chunk
);
},
functionCall: function(fn, type, params) {
@@ -113,26 +116,28 @@ CodeGen.prototype = {
},
quotedString: function(str) {
return '"' + (str + '')
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
.replace(/\u2029/g, '\\u2029') + '"';
return (
'"' +
(str + '')
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
.replace(/\u2029/g, '\\u2029') +
'"'
);
},
objectLiteral: function(obj) {
let pairs = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
let value = castChunk(obj[key], this);
if (value !== 'undefined') {
pairs.push([this.quotedString(key), ':', value]);
}
Object.keys(obj).forEach(key => {
let value = castChunk(obj[key], this);
if (value !== 'undefined') {
pairs.push([this.quotedString(key), ':', value]);
}
}
});
let ret = this.generateList(pairs);
ret.prepend('{');
@@ -140,7 +145,6 @@ CodeGen.prototype = {
return ret;
},
generateList: function(entries) {
let ret = this.empty();
@@ -165,4 +169,3 @@ CodeGen.prototype = {
};
export default CodeGen;
+97 -62
View File
@@ -1,7 +1,7 @@
/* eslint-disable new-cap */
import Exception from '../exception';
import {isArray, indexOf, extend} from '../utils';
import { isArray, indexOf, extend } from '../utils';
import AST from './ast';
const slice = [].slice;
@@ -24,8 +24,11 @@ Compiler.prototype = {
for (let i = 0; i < len; i++) {
let opcode = this.opcodes[i],
otherOpcode = other.opcodes[i];
if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
otherOpcode = other.opcodes[i];
if (
opcode.opcode !== otherOpcode.opcode ||
!argEquals(opcode.args, otherOpcode.args)
) {
return false;
}
}
@@ -54,34 +57,28 @@ Compiler.prototype = {
options.blockParams = options.blockParams || [];
// These changes will propagate to the other compiler components
let knownHelpers = options.knownHelpers;
options.knownHelpers = {
'helperMissing': true,
'blockHelperMissing': true,
'each': true,
'if': true,
'unless': true,
'with': true,
'log': true,
'lookup': true
};
if (knownHelpers) {
// the next line should use "Object.keys", but the code has been like this a long time and changing it, might
// cause backwards-compatibility issues... It's an old library...
// eslint-disable-next-line guard-for-in
for (let name in knownHelpers) {
this.options.knownHelpers[name] = knownHelpers[name];
}
}
options.knownHelpers = extend(
Object.create(null),
{
helperMissing: true,
blockHelperMissing: true,
each: true,
if: true,
unless: true,
with: true,
log: true,
lookup: true
},
options.knownHelpers
);
return this.accept(program);
},
compileProgram: function(program) {
let childCompiler = new this.compiler(), // eslint-disable-line new-cap
result = childCompiler.compile(program, this.options),
guid = this.guid++;
result = childCompiler.compile(program, this.options),
guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial;
@@ -107,7 +104,7 @@ Compiler.prototype = {
this.options.blockParams.unshift(program.blockParams);
let body = program.body,
bodyLength = body.length;
bodyLength = body.length;
for (let i = 0; i < bodyLength; i++) {
this.accept(body[i]);
}
@@ -124,7 +121,7 @@ Compiler.prototype = {
transformLiteralToPath(block);
let program = block.program,
inverse = block.inverse;
inverse = block.inverse;
program = program && this.compileProgram(program);
inverse = inverse && this.compileProgram(inverse);
@@ -159,7 +156,7 @@ Compiler.prototype = {
DecoratorBlock(decorator) {
let program = decorator.program && this.compileProgram(decorator.program);
let params = this.setupFullMustacheParams(decorator, program, undefined),
path = decorator.path;
path = decorator.path;
this.useDecorators = true;
this.opcode('registerDecorator', params.length, path.original);
@@ -175,17 +172,20 @@ Compiler.prototype = {
let params = partial.params;
if (params.length > 1) {
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
throw new Exception(
'Unsupported number of partial arguments: ' + params.length,
partial
);
} else if (!params.length) {
if (this.options.explicitPartialContext) {
this.opcode('pushLiteral', 'undefined');
} else {
params.push({type: 'PathExpression', parts: [], depth: 0});
params.push({ type: 'PathExpression', parts: [], depth: 0 });
}
}
let partialName = partial.name.original,
isDynamic = partial.name.type === 'SubExpression';
isDynamic = partial.name.type === 'SubExpression';
if (isDynamic) {
this.accept(partial.name);
}
@@ -218,7 +218,6 @@ Compiler.prototype = {
this.DecoratorBlock(decorator);
},
ContentStatement: function(content) {
if (content.value) {
this.opcode('appendContent', content.value);
@@ -241,8 +240,8 @@ Compiler.prototype = {
},
ambiguousSexpr: function(sexpr, program, inverse) {
let path = sexpr.path,
name = path.parts[0],
isBlock = program != null || inverse != null;
name = path.parts[0],
isBlock = program != null || inverse != null;
this.opcode('getContext', path.depth);
@@ -264,19 +263,27 @@ Compiler.prototype = {
helperSexpr: function(sexpr, program, inverse) {
let params = this.setupFullMustacheParams(sexpr, program, inverse),
path = sexpr.path,
name = path.parts[0];
path = sexpr.path,
name = path.parts[0];
if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) {
throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr);
throw new Exception(
'You specified knownHelpersOnly, but used the unknown helper ' + name,
sexpr
);
} else {
path.strict = true;
path.falsy = true;
this.accept(path);
this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path));
this.opcode(
'invokeHelper',
params.length,
path.original,
AST.helpers.simpleId(path)
);
}
},
@@ -285,8 +292,8 @@ Compiler.prototype = {
this.opcode('getContext', path.depth);
let name = path.parts[0],
scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
if (blockParamId) {
this.opcode('lookupBlockParam', blockParamId, path.parts);
@@ -297,7 +304,13 @@ Compiler.prototype = {
this.options.data = true;
this.opcode('lookupData', path.depth, path.parts, path.strict);
} else {
this.opcode('lookupOnContext', path.parts, path.falsy, path.strict, scoped);
this.opcode(
'lookupOnContext',
path.parts,
path.falsy,
path.strict,
scoped
);
}
},
@@ -323,8 +336,8 @@ Compiler.prototype = {
Hash: function(hash) {
let pairs = hash.pairs,
i = 0,
l = pairs.length;
i = 0,
l = pairs.length;
this.opcode('pushHash');
@@ -339,7 +352,11 @@ Compiler.prototype = {
// HELPERS
opcode: function(name) {
this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc });
this.opcodes.push({
opcode: name,
args: slice.call(arguments, 1),
loc: this.sourceNode[0].loc
});
},
addDepth: function(depth) {
@@ -368,8 +385,7 @@ Compiler.prototype = {
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
if (isEligible && !isHelper) {
let name = sexpr.path.parts[0],
options = this.options;
options = this.options;
if (options.knownHelpers[name]) {
isHelper = true;
} else if (options.knownHelpersOnly) {
@@ -397,9 +413,7 @@ Compiler.prototype = {
if (this.stringParams) {
if (value.replace) {
value = value
.replace(/^(\.?\.\/)*/g, '')
.replace(/\//g, '.');
value = value.replace(/^(\.?\.\/)*/g, '').replace(/\//g, '.');
}
if (val.depth) {
@@ -417,7 +431,7 @@ Compiler.prototype = {
if (this.trackIds) {
let blockParamIndex;
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
blockParamIndex = this.blockParamIndex(val.parts[0]);
blockParamIndex = this.blockParamIndex(val.parts[0]);
}
if (blockParamIndex) {
let blockParamChild = val.parts.slice(1).join('.');
@@ -426,9 +440,9 @@ Compiler.prototype = {
value = val.original || value;
if (value.replace) {
value = value
.replace(/^this(?:\.|$)/, '')
.replace(/^\.\//, '')
.replace(/^\.$/, '');
.replace(/^this(?:\.|$)/, '')
.replace(/^\.\//, '')
.replace(/^\.$/, '');
}
this.opcode('pushId', val.type, value);
@@ -455,9 +469,13 @@ Compiler.prototype = {
},
blockParamIndex: function(name) {
for (let depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
for (
let depth = 0, len = this.options.blockParams.length;
depth < len;
depth++
) {
let blockParams = this.options.blockParams[depth],
param = blockParams && indexOf(blockParams, name);
param = blockParams && indexOf(blockParams, name);
if (blockParams && param >= 0) {
return [depth, param];
}
@@ -466,8 +484,14 @@ Compiler.prototype = {
};
export function precompile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception('You must pass a string or Handlebars AST to Handlebars.precompile. You passed ' + input);
if (
input == null ||
(typeof input !== 'string' && input.type !== 'Program')
) {
throw new Exception(
'You must pass a string or Handlebars AST to Handlebars.precompile. You passed ' +
input
);
}
options = options || {};
@@ -479,13 +503,19 @@ export function precompile(input, options, env) {
}
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options);
environment = new env.Compiler().compile(ast, options);
return new env.JavaScriptCompiler().compile(environment, options);
}
export function compile(input, options = {}, env) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception('You must pass a string or Handlebars AST to Handlebars.compile. You passed ' + input);
if (
input == null ||
(typeof input !== 'string' && input.type !== 'Program')
) {
throw new Exception(
'You must pass a string or Handlebars AST to Handlebars.compile. You passed ' +
input
);
}
options = extend({}, options);
@@ -500,8 +530,13 @@ export function compile(input, options = {}, env) {
function compileInput() {
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options),
templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
environment = new env.Compiler().compile(ast, options),
templateSpec = new env.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
return env.template(templateSpec);
}
+29 -20
View File
@@ -4,9 +4,12 @@ function validateClose(open, close) {
close = close.path ? close.path.original : close;
if (open.path.original !== close) {
let errorNode = {loc: open.path.loc};
let errorNode = { loc: open.path.loc };
throw new Exception(open.path.original + " doesn't match " + close, errorNode);
throw new Exception(
open.path.original + " doesn't match " + close,
errorNode
);
}
}
@@ -38,27 +41,26 @@ export function stripFlags(open, close) {
}
export function stripComment(comment) {
return comment.replace(/^\{\{~?!-?-?/, '')
.replace(/-?-?~?\}\}$/, '');
return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, '');
}
export function preparePath(data, parts, loc) {
loc = this.locInfo(loc);
let original = data ? '@' : '',
dig = [],
depth = 0;
dig = [],
depth = 0;
for (let i = 0, l = parts.length; i < l; i++) {
let part = parts[i].part,
// If we have [] syntax then we do not treat path references as operators,
// i.e. foo.[this] resolves to approximately context.foo['this']
isLiteral = parts[i].original !== part;
// If we have [] syntax then we do not treat path references as operators,
// i.e. foo.[this] resolves to approximately context.foo['this']
isLiteral = parts[i].original !== part;
original += (parts[i].separator || '') + part;
if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
if (dig.length > 0) {
throw new Exception('Invalid path: ' + original, {loc});
throw new Exception('Invalid path: ' + original, { loc });
} else if (part === '..') {
depth++;
}
@@ -80,9 +82,9 @@ export function preparePath(data, parts, loc) {
export function prepareMustache(path, params, hash, open, strip, locInfo) {
// Must use charAt to support IE pre-10
let escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';
escaped = escapeFlag !== '{' && escapeFlag !== '&';
let decorator = (/\*/.test(open));
let decorator = /\*/.test(open);
return {
type: decorator ? 'Decorator' : 'MustacheStatement',
path,
@@ -118,21 +120,30 @@ export function prepareRawBlock(openRawBlock, contents, close, locInfo) {
};
}
export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
export function prepareBlock(
openBlock,
program,
inverseAndProgram,
close,
inverted,
locInfo
) {
if (close && close.path) {
validateClose(openBlock, close);
}
let decorator = (/\*/.test(openBlock.open));
let decorator = /\*/.test(openBlock.open);
program.blockParams = openBlock.blockParams;
let inverse,
inverseStrip;
let inverse, inverseStrip;
if (inverseAndProgram) {
if (decorator) {
throw new Exception('Unexpected inverse block on decorator', inverseAndProgram);
throw new Exception(
'Unexpected inverse block on decorator',
inverseAndProgram
);
}
if (inverseAndProgram.chain) {
@@ -166,7 +177,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver
export function prepareProgram(statements, loc) {
if (!loc && statements.length) {
const firstLoc = statements[0].loc,
lastLoc = statements[statements.length - 1].loc;
lastLoc = statements[statements.length - 1].loc;
/* istanbul ignore else */
if (firstLoc && lastLoc) {
@@ -192,7 +203,6 @@ export function prepareProgram(statements, loc) {
};
}
export function preparePartialBlock(open, program, close, locInfo) {
validateClose(open, close);
@@ -207,4 +217,3 @@ export function preparePartialBlock(open, program, close, locInfo) {
loc: this.locInfo(locInfo)
};
}
+241 -108
View File
@@ -1,6 +1,6 @@
import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
import Exception from '../exception';
import {isArray} from '../utils';
import { isArray } from '../utils';
import CodeGen from './code-gen';
function Literal(value) {
@@ -12,29 +12,21 @@ function JavaScriptCompiler() {}
JavaScriptCompiler.prototype = {
// PUBLIC API: You can override these methods in a subclass to provide
// alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name/* , type*/) {
const isEnumerable = [ this.aliasable('container.propertyIsEnumerable'), '.call(', parent, ',"constructor")'];
if (name === 'constructor') {
return ['(', isEnumerable, '?', _actualLookup(), ' : undefined)'];
}
return _actualLookup();
function _actualLookup() {
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return [parent, '.', name];
} else {
return [parent, '[', JSON.stringify(name), ']'];
}
}
nameLookup: function(parent, name /*, type */) {
return this.internalNameLookup(parent, name);
},
depthedLookup: function(name) {
return [this.aliasable('container.lookup'), '(depths, "', name, '")'];
return [
this.aliasable('container.lookup'),
'(depths, ',
JSON.stringify(name),
')'
];
},
compilerInfo: function() {
const revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
versions = REVISION_CHANGES[revision];
return [revision, versions];
},
@@ -62,6 +54,12 @@ JavaScriptCompiler.prototype = {
return this.quotedString('');
},
// END PUBLIC API
internalNameLookup: function(parent, name) {
this.lookupPropertyFunctionIsUsed = true;
return ['lookupProperty(', parent, ',', JSON.stringify(name), ')'];
},
lookupPropertyFunctionIsUsed: false,
compile: function(environment, options, context, asObject) {
this.environment = environment;
@@ -91,14 +89,18 @@ JavaScriptCompiler.prototype = {
this.compileChildren(environment, options);
this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat;
this.useDepths =
this.useDepths ||
environment.useDepths ||
environment.useDecorators ||
this.options.compat;
this.useBlockParams = this.useBlockParams || environment.useBlockParams;
let opcodes = environment.opcodes,
opcode,
firstLoc,
i,
l;
opcode,
firstLoc,
i,
l;
for (i = 0, l = opcodes.length; i < l; i++) {
opcode = opcodes[i];
@@ -120,13 +122,28 @@ JavaScriptCompiler.prototype = {
if (!this.decorators.isEmpty()) {
this.useDecorators = true;
this.decorators.prepend('var decorators = container.decorators;\n');
this.decorators.prepend([
'var decorators = container.decorators, ',
this.lookupPropertyFunctionVarDeclaration(),
';\n'
]);
this.decorators.push('return fn;');
if (asObject) {
this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]);
this.decorators = Function.apply(this, [
'fn',
'props',
'container',
'depth0',
'data',
'blockParams',
'depths',
this.decorators.merge()
]);
} else {
this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n');
this.decorators.prepend(
'function(fn, props, container, depth0, data, blockParams, depths) {\n'
);
this.decorators.push('}\n');
this.decorators = this.decorators.merge();
}
@@ -146,7 +163,7 @@ JavaScriptCompiler.prototype = {
ret.useDecorators = true;
}
let {programs, decorators} = this.context;
let { programs, decorators } = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
@@ -176,11 +193,11 @@ JavaScriptCompiler.prototype = {
if (!asObject) {
ret.compiler = JSON.stringify(ret.compiler);
this.source.currentLocation = {start: {line: 1, column: 0}};
this.source.currentLocation = { start: { line: 1, column: 0 } };
ret = this.objectLiteral(ret);
if (options.srcName) {
ret = ret.toStringWithSourceMap({file: options.destName});
ret = ret.toStringWithSourceMap({ file: options.destName });
ret.map = ret.map && ret.map.toString();
} else {
ret = ret.toString();
@@ -218,12 +235,16 @@ JavaScriptCompiler.prototype = {
// aliases will not be used, but this case is already being run on the client and
// we aren't concern about minimizing the template size.
let aliasCount = 0;
for (let alias in this.aliases) { // eslint-disable-line guard-for-in
Object.keys(this.aliases).forEach(alias => {
let node = this.aliases[alias];
if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) {
varDeclarations += ', alias' + (++aliasCount) + '=' + alias;
if (node.children && node.referenceCount > 1) {
varDeclarations += ', alias' + ++aliasCount + '=' + alias;
node.children[0] = 'alias' + aliasCount;
}
});
if (this.lookupPropertyFunctionIsUsed) {
varDeclarations += ', ' + this.lookupPropertyFunctionVarDeclaration();
}
let params = ['container', 'depth0', 'helpers', 'partials', 'data'];
@@ -243,18 +264,23 @@ JavaScriptCompiler.prototype = {
return Function.apply(this, params);
} else {
return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']);
return this.source.wrap([
'function(',
params.join(','),
') {\n ',
source,
'}'
]);
}
},
mergeSource: function(varDeclarations) {
let isSimple = this.environment.isSimple,
appendOnly = !this.forceBuffer,
appendFirst,
sourceSeen,
bufferStart,
bufferEnd;
this.source.each((line) => {
appendOnly = !this.forceBuffer,
appendFirst,
sourceSeen,
bufferStart,
bufferEnd;
this.source.each(line => {
if (line.appendToBuffer) {
if (bufferStart) {
line.prepend(' + ');
@@ -280,16 +306,16 @@ JavaScriptCompiler.prototype = {
}
});
if (appendOnly) {
if (bufferStart) {
bufferStart.prepend('return ');
bufferStart.prepend('return ""+');
bufferEnd.add(';');
} else if (!sourceSeen) {
this.source.push('return "";');
}
} else {
varDeclarations += ', buffer = ' + (appendFirst ? '' : this.initializeBuffer());
varDeclarations +=
', buffer = ' + (appendFirst ? '' : this.initializeBuffer());
if (bufferStart) {
bufferStart.prepend('return buffer + ');
@@ -300,12 +326,25 @@ JavaScriptCompiler.prototype = {
}
if (varDeclarations) {
this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n'));
this.source.prepend(
'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n')
);
}
return this.source.merge();
},
lookupPropertyFunctionVarDeclaration: function() {
return `
lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined
}
`.trim();
},
// [blockValue]
//
// On stack, before: hash, inverse, program, value
@@ -316,8 +355,10 @@ JavaScriptCompiler.prototype = {
// replace it on the stack with the result of properly
// invoking blockHelperMissing.
blockValue: function(name) {
let blockHelperMissing = this.aliasable('container.hooks.blockHelperMissing'),
params = [this.contextName(0)];
let blockHelperMissing = this.aliasable(
'container.hooks.blockHelperMissing'
),
params = [this.contextName(0)];
this.setupHelperArgs(name, 0, params);
let blockName = this.popStack();
@@ -334,8 +375,10 @@ JavaScriptCompiler.prototype = {
// On stack, after, if lastHelper: value
ambiguousBlockValue: function() {
// We're being a bit cheeky and reusing the options value from the prior exec
let blockHelperMissing = this.aliasable('container.hooks.blockHelperMissing'),
params = [this.contextName(0)];
let blockHelperMissing = this.aliasable(
'container.hooks.blockHelperMissing'
),
params = [this.contextName(0)];
this.setupHelperArgs('', 0, params, true);
this.flushInline();
@@ -344,9 +387,14 @@ JavaScriptCompiler.prototype = {
params.splice(1, 0, current);
this.pushSource([
'if (!', this.lastHelper, ') { ',
current, ' = ', this.source.functionCall(blockHelperMissing, 'call', params),
'}']);
'if (!',
this.lastHelper,
') { ',
current,
' = ',
this.source.functionCall(blockHelperMissing, 'call', params),
'}'
]);
},
// [appendContent]
@@ -376,14 +424,24 @@ JavaScriptCompiler.prototype = {
// Otherwise, the empty string is appended
append: function() {
if (this.isInline()) {
this.replaceStack((current) => [' != null ? ', current, ' : ""']);
this.replaceStack(current => [' != null ? ', current, ' : ""']);
this.pushSource(this.appendToBuffer(this.popStack()));
} else {
let local = this.popStack();
this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']);
this.pushSource([
'if (',
local,
' != null) { ',
this.appendToBuffer(local, undefined, true),
' }'
]);
if (this.environment.isSimple) {
this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']);
this.pushSource([
'else { ',
this.appendToBuffer("''", undefined, true),
' }'
]);
}
}
},
@@ -395,8 +453,14 @@ JavaScriptCompiler.prototype = {
//
// Escape `value` and append it to the buffer
appendEscaped: function() {
this.pushSource(this.appendToBuffer(
[this.aliasable('container.escapeExpression'), '(', this.popStack(), ')']));
this.pushSource(
this.appendToBuffer([
this.aliasable('container.escapeExpression'),
'(',
this.popStack(),
')'
])
);
},
// [getContext]
@@ -473,14 +537,16 @@ JavaScriptCompiler.prototype = {
resolvePath: function(type, parts, i, falsy, strict) {
if (this.options.strict || this.options.assumeObjects) {
this.push(strictLookup(this.options.strict && strict, this, parts, type));
this.push(
strictLookup(this.options.strict && strict, this, parts, i, type)
);
return;
}
let len = parts.length;
for (; i < len; i++) {
/* eslint-disable no-loop-func */
this.replaceStack((current) => {
this.replaceStack(current => {
let lookup = this.nameLookup(current, parts[i], type);
// We want to ensure that zero and false are handled properly if the context (falsy flag)
// needs to have the special handling for these values.
@@ -503,7 +569,14 @@ JavaScriptCompiler.prototype = {
// If the `value` is a lambda, replace it on the stack by
// the return value of the lambda
resolvePossibleLambda: function() {
this.push([this.aliasable('container.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']);
this.push([
this.aliasable('container.lambda'),
'(',
this.popStack(),
', ',
this.contextName(0),
')'
]);
},
// [pushStringParam]
@@ -543,7 +616,7 @@ JavaScriptCompiler.prototype = {
if (this.hash) {
this.hashes.push(this.hash);
}
this.hash = {values: {}, types: [], contexts: [], ids: []};
this.hash = { values: {}, types: [], contexts: [], ids: [] };
},
popHash: function() {
let hash = this.hash;
@@ -607,11 +680,16 @@ JavaScriptCompiler.prototype = {
// and inserts the decorator into the decorators list.
registerDecorator(paramSize, name) {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);
options = this.setupHelperArgs(name, paramSize);
this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]),
this.decorators.functionCall(foundDecorator, '', [
'fn',
'props',
'container',
options
]),
' || fn;'
]);
},
@@ -627,21 +705,32 @@ JavaScriptCompiler.prototype = {
// If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name, isSimple) {
let nonHelper = this.popStack(),
helper = this.setupHelper(paramSize, name);
helper = this.setupHelper(paramSize, name);
let possibleFunctionCalls = [];
if (isSimple) { // direct call to helper
if (isSimple) {
// direct call to helper
possibleFunctionCalls.push(helper.name);
}
// call a function from the input object
possibleFunctionCalls.push(nonHelper);
if (!this.options.strict) {
possibleFunctionCalls.push(this.aliasable('container.hooks.helperMissing'));
possibleFunctionCalls.push(
this.aliasable('container.hooks.helperMissing')
);
}
let functionLookupCode = ['(', this.itemsSeparatedBy(possibleFunctionCalls, '||'), ')'];
let functionCall = this.source.functionCall(functionLookupCode, 'call', helper.callParams);
let functionLookupCode = [
'(',
this.itemsSeparatedBy(possibleFunctionCalls, '||'),
')'
];
let functionCall = this.source.functionCall(
functionLookupCode,
'call',
helper.callParams
);
this.push(functionCall);
},
@@ -685,22 +774,31 @@ JavaScriptCompiler.prototype = {
this.emptyHash();
let helper = this.setupHelper(0, name, helperCall);
let helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
let helperName = (this.lastHelper = this.nameLookup(
'helpers',
name,
'helper'
));
let lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')'];
if (!this.options.strict) {
lookup[0] = '(helper = ';
lookup.push(
' != null ? helper : ',
this.aliasable('container.hooks.helperMissing')
' != null ? helper : ',
this.aliasable('container.hooks.helperMissing')
);
}
this.push([
'(', lookup,
(helper.paramsInit ? ['),(', helper.paramsInit] : []), '),',
'(typeof helper === ', this.aliasable('"function"'), ' ? ',
this.source.functionCall('helper', 'call', helper.callParams), ' : helper))'
'(',
lookup,
helper.paramsInit ? ['),(', helper.paramsInit] : [],
'),',
'(typeof helper === ',
this.aliasable('"function"'),
' ? ',
this.source.functionCall('helper', 'call', helper.callParams),
' : helper))'
]);
},
@@ -713,7 +811,7 @@ JavaScriptCompiler.prototype = {
// and pushes the result of the invocation back.
invokePartial: function(isDynamic, name, indent) {
let params = [],
options = this.setupParams(name, 1, params);
options = this.setupParams(name, 1, params);
if (isDynamic) {
name = this.popStack();
@@ -750,9 +848,9 @@ JavaScriptCompiler.prototype = {
// Pops a value off the stack and assigns it to the current hash
assignToHash: function(key) {
let value = this.popStack(),
context,
type,
id;
context,
type,
id;
if (this.trackIds) {
id = this.popStack();
@@ -778,8 +876,13 @@ JavaScriptCompiler.prototype = {
pushId: function(type, name, child) {
if (type === 'BlockParam') {
this.pushStackLiteral(
'blockParams[' + name[0] + '].path[' + name[1] + ']'
+ (child ? ' + ' + JSON.stringify('.' + child) : ''));
'blockParams[' +
name[0] +
'].path[' +
name[1] +
']' +
(child ? ' + ' + JSON.stringify('.' + child) : '')
);
} else if (type === 'PathExpression') {
this.pushString(name);
} else if (type === 'SubExpression') {
@@ -794,7 +897,9 @@ JavaScriptCompiler.prototype = {
compiler: JavaScriptCompiler,
compileChildren: function(environment, options) {
let children = environment.children, child, compiler;
let children = environment.children,
child,
compiler;
for (let i = 0, l = children.length; i < l; i++) {
child = children[i];
@@ -807,7 +912,12 @@ JavaScriptCompiler.prototype = {
let index = this.context.programs.length;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.programs[index] = compiler.compile(
child,
options,
this.context,
!this.precompile
);
this.context.decorators[index] = compiler.decorators;
this.context.environments[index] = child;
@@ -835,7 +945,7 @@ JavaScriptCompiler.prototype = {
programExpression: function(guid) {
let child = this.environment.children[guid],
programParams = [child.index, 'data', child.blockParams];
programParams = [child.index, 'data', child.blockParams];
if (this.useBlockParams || this.useDepths) {
programParams.push('blockParams');
@@ -870,7 +980,11 @@ JavaScriptCompiler.prototype = {
pushSource: function(source) {
if (this.pendingContent) {
this.source.push(
this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation));
this.appendToBuffer(
this.source.quotedString(this.pendingContent),
this.pendingLocation
)
);
this.pendingContent = undefined;
}
@@ -881,9 +995,9 @@ JavaScriptCompiler.prototype = {
replaceStack: function(callback) {
let prefix = ['('],
stack,
createdStack,
usedLiteral;
stack,
createdStack,
usedLiteral;
/* istanbul ignore next */
if (!this.isInline()) {
@@ -920,7 +1034,9 @@ JavaScriptCompiler.prototype = {
incrStack: function() {
this.stackSlot++;
if (this.stackSlot > this.stackVars.length) { this.stackVars.push('stack' + this.stackSlot); }
if (this.stackSlot > this.stackVars.length) {
this.stackVars.push('stack' + this.stackSlot);
}
return this.topStackName();
},
topStackName: function() {
@@ -947,9 +1063,9 @@ JavaScriptCompiler.prototype = {
popStack: function(wrapped) {
let inline = this.isInline(),
item = (inline ? this.inlineStack : this.compileStack).pop();
item = (inline ? this.inlineStack : this.compileStack).pop();
if (!wrapped && (item instanceof Literal)) {
if (!wrapped && item instanceof Literal) {
return item.value;
} else {
if (!inline) {
@@ -964,8 +1080,8 @@ JavaScriptCompiler.prototype = {
},
topStack: function() {
let stack = (this.isInline() ? this.inlineStack : this.compileStack),
item = stack[stack.length - 1];
let stack = this.isInline() ? this.inlineStack : this.compileStack,
item = stack[stack.length - 1];
/* istanbul ignore if */
if (item instanceof Literal) {
@@ -1007,9 +1123,13 @@ JavaScriptCompiler.prototype = {
setupHelper: function(paramSize, name, blockHelper) {
let params = [],
paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
let foundHelper = this.nameLookup('helpers', name, 'helper'),
callContext = this.aliasable(`${this.contextName(0)} != null ? ${this.contextName(0)} : (container.nullContext || {})`);
callContext = this.aliasable(
`${this.contextName(0)} != null ? ${this.contextName(
0
)} : (container.nullContext || {})`
);
return {
params: params,
@@ -1021,11 +1141,11 @@ JavaScriptCompiler.prototype = {
setupParams: function(helper, paramSize, params) {
let options = {},
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;
if (objectArgs) {
params = [];
@@ -1043,7 +1163,7 @@ JavaScriptCompiler.prototype = {
}
let inverse = this.popStack(),
program = this.popStack();
program = this.popStack();
// Avoid setting fn and inverse if neither are set. This allows
// helpers to do a check for `if (options.fn)`
@@ -1106,7 +1226,6 @@ JavaScriptCompiler.prototype = {
}
};
(function() {
const reservedWords = (
'break else new var' +
@@ -1127,21 +1246,26 @@ JavaScriptCompiler.prototype = {
' null true false'
).split(' ');
const compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
const compilerWords = (JavaScriptCompiler.RESERVED_WORDS = {});
for (let i = 0, l = reservedWords.length; i < l; i++) {
compilerWords[reservedWords[i]] = true;
}
}());
})();
/**
* @deprecated May be removed in the next major version
*/
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
return !JavaScriptCompiler.RESERVED_WORDS[name] && (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/).test(name);
return (
!JavaScriptCompiler.RESERVED_WORDS[name] &&
/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name)
);
};
function strictLookup(requireTerminal, compiler, parts, type) {
function strictLookup(requireTerminal, compiler, parts, i, type) {
let stack = compiler.popStack(),
i = 0,
len = parts.length;
len = parts.length;
if (requireTerminal) {
len--;
}
@@ -1151,7 +1275,16 @@ function strictLookup(requireTerminal, compiler, parts, type) {
}
if (requireTerminal) {
return [compiler.aliasable('container.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ', ', JSON.stringify(compiler.source.currentLocation), ' )'];
return [
compiler.aliasable('container.strict'),
'(',
stack,
', ',
compiler.quotedString(parts[i]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )'
];
} else {
return stack;
}
+19 -12
View File
@@ -24,13 +24,14 @@ PrintVisitor.prototype.pad = function(string) {
PrintVisitor.prototype.Program = function(program) {
let out = '',
body = program.body,
i, l;
body = program.body,
i,
l;
if (program.blockParams) {
let blockParams = 'BLOCK PARAMS: [';
for (i = 0, l = program.blockParams.length; i < l; i++) {
blockParams += ' ' + program.blockParams[i];
blockParams += ' ' + program.blockParams[i];
}
blockParams += ' ]';
out += this.pad(blockParams);
@@ -52,11 +53,14 @@ PrintVisitor.prototype.Decorator = function(mustache) {
return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.BlockStatement =
PrintVisitor.prototype.DecoratorBlock = function(block) {
PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = function(
block
) {
let out = '';
out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:');
out += this.pad(
(block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'
);
this.padding++;
out += this.pad(this.SubExpression(block));
if (block.program) {
@@ -66,12 +70,16 @@ PrintVisitor.prototype.DecoratorBlock = function(block) {
this.padding--;
}
if (block.inverse) {
if (block.program) { this.padding++; }
if (block.program) {
this.padding++;
}
out += this.pad('{{^}}');
this.padding++;
out += this.accept(block.inverse);
this.padding--;
if (block.program) { this.padding--; }
if (block.program) {
this.padding--;
}
}
this.padding--;
@@ -115,8 +123,8 @@ PrintVisitor.prototype.CommentStatement = function(comment) {
PrintVisitor.prototype.SubExpression = function(sexpr) {
let params = sexpr.params,
paramStrings = [],
hash;
paramStrings = [],
hash;
for (let i = 0, l = params.length; i < l; i++) {
paramStrings.push(this.accept(params[i]));
@@ -134,7 +142,6 @@ PrintVisitor.prototype.PathExpression = function(id) {
return (id.data ? '@' : '') + 'PATH:' + path;
};
PrintVisitor.prototype.StringLiteral = function(string) {
return '"' + string.value + '"';
};
@@ -157,7 +164,7 @@ PrintVisitor.prototype.NullLiteral = function() {
PrintVisitor.prototype.Hash = function(hash) {
let pairs = hash.pairs,
joinedPairs = [];
joinedPairs = [];
for (let i = 0, l = pairs.length; i < l; i++) {
joinedPairs.push(this.accept(pairs[i]));
+8 -1
View File
@@ -15,7 +15,14 @@ Visitor.prototype = {
// Hacky sanity check: This may have a few false positives for type for the helper
// methods but will generally do the right thing without a lot of overhead.
if (value && !Visitor.prototype[value.type]) {
throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type);
throw new Exception(
'Unexpected node type "' +
value.type +
'" found when accepting ' +
name +
' on ' +
node.type
);
}
node[name] = value;
}
+47 -29
View File
@@ -14,18 +14,18 @@ WhitespaceControl.prototype.Program = function(program) {
let body = program.body;
for (let i = 0, l = body.length; i < l; i++) {
let current = body[i],
strip = this.accept(current);
strip = this.accept(current);
if (!strip) {
continue;
}
let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
_isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
closeStandalone = strip.closeStandalone && _isNextWhitespace,
inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
_isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
closeStandalone = strip.closeStandalone && _isNextWhitespace,
inlineStandalone =
strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
if (strip.close) {
omitRight(body, i, true);
@@ -41,7 +41,7 @@ WhitespaceControl.prototype.Program = function(program) {
// If we are on a standalone node, save the indent info for partials
if (current.type === 'PartialStatement') {
// Pull out the whitespace from the final line
current.indent = (/([ \t]+$)/).exec(body[i - 1].original)[1];
current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1];
}
}
}
@@ -62,17 +62,17 @@ WhitespaceControl.prototype.Program = function(program) {
return program;
};
WhitespaceControl.prototype.BlockStatement =
WhitespaceControl.prototype.DecoratorBlock =
WhitespaceControl.prototype.PartialBlockStatement = function(block) {
WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function(
block
) {
this.accept(block.program);
this.accept(block.inverse);
// Find the inverse program that is involed with whitespace stripping.
let program = block.program || block.inverse,
inverse = block.program && block.inverse,
firstInverse = inverse,
lastInverse = inverse;
inverse = block.program && block.inverse,
firstInverse = inverse,
lastInverse = inverse;
if (inverse && inverse.chained) {
firstInverse = inverse.body[0].program;
@@ -112,9 +112,11 @@ WhitespaceControl.prototype.PartialBlockStatement = function(block) {
}
// Find standalone else statments
if (!this.options.ignoreStandalone
&& isPrevWhitespace(program.body)
&& isNextWhitespace(firstInverse.body)) {
if (
!this.options.ignoreStandalone &&
isPrevWhitespace(program.body) &&
isNextWhitespace(firstInverse.body)
) {
omitLeft(program.body);
omitRight(firstInverse.body);
}
@@ -125,13 +127,15 @@ WhitespaceControl.prototype.PartialBlockStatement = function(block) {
return strip;
};
WhitespaceControl.prototype.Decorator =
WhitespaceControl.prototype.MustacheStatement = function(mustache) {
WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(
mustache
) {
return mustache.strip;
};
WhitespaceControl.prototype.PartialStatement =
WhitespaceControl.prototype.CommentStatement = function(node) {
WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function(
node
) {
/* istanbul ignore next */
let strip = node.strip || {};
return {
@@ -141,7 +145,6 @@ WhitespaceControl.prototype.PartialStatement =
};
};
function isPrevWhitespace(body, i, isRoot) {
if (i === undefined) {
i = body.length;
@@ -150,13 +153,15 @@ function isPrevWhitespace(body, i, isRoot) {
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
let prev = body[i - 1],
sibling = body[i - 2];
sibling = body[i - 2];
if (!prev) {
return isRoot;
}
if (prev.type === 'ContentStatement') {
return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original);
return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(
prev.original
);
}
}
function isNextWhitespace(body, i, isRoot) {
@@ -165,13 +170,15 @@ function isNextWhitespace(body, i, isRoot) {
}
let next = body[i + 1],
sibling = body[i + 2];
sibling = body[i + 2];
if (!next) {
return isRoot;
}
if (next.type === 'ContentStatement') {
return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original);
return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test(
next.original
);
}
}
@@ -184,12 +191,19 @@ function isNextWhitespace(body, i, isRoot) {
// content is met.
function omitRight(body, i, multiple) {
let current = body[i == null ? 0 : i + 1];
if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) {
if (
!current ||
current.type !== 'ContentStatement' ||
(!multiple && current.rightStripped)
) {
return;
}
let original = current.value;
current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
current.value = current.value.replace(
multiple ? /^\s+/ : /^[ \t]*\r?\n?/,
''
);
current.rightStripped = current.value !== original;
}
@@ -202,13 +216,17 @@ function omitRight(body, i, multiple) {
// content is met.
function omitLeft(body, i, multiple) {
let current = body[i == null ? body.length - 1 : i - 1];
if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) {
if (
!current ||
current.type !== 'ContentStatement' ||
(!multiple && current.leftStripped)
) {
return;
}
// We omit the last node if it's whitespace only and not preceded by a non-content node.
let original = current.value;
current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, '');
current.leftStripped = current.value !== original;
return current.leftStripped;
}
-1
View File
@@ -3,4 +3,3 @@ import registerInline from './decorators/inline';
export function registerDefaultDecorators(instance) {
registerInline(instance);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import {extend} from '../utils';
import { extend } from '../utils';
export default function(instance) {
instance.registerDecorator('inline', function(fn, props, container, options) {
+14 -6
View File
@@ -1,12 +1,20 @@
const errorProps = ['description', 'fileName', 'lineNumber', 'endLineNumber', 'message', 'name', 'number', 'stack'];
const errorProps = [
'description',
'fileName',
'lineNumber',
'endLineNumber',
'message',
'name',
'number',
'stack'
];
function Exception(message, node) {
let loc = node && node.loc,
line,
endLineNumber,
column,
endColumn;
line,
endLineNumber,
column,
endColumn;
if (loc) {
line = loc.start.line;
@@ -1,9 +1,9 @@
import {appendContextPath, createFrame, isArray} from '../utils';
import { appendContextPath, createFrame, isArray } from '../utils';
export default function(instance) {
instance.registerHelper('blockHelperMissing', function(context, options) {
let inverse = options.inverse,
fn = options.fn;
fn = options.fn;
if (context === true) {
return fn(this);
@@ -22,8 +22,11 @@ export default function(instance) {
} else {
if (options.data && options.ids) {
let data = createFrame(options.data);
data.contextPath = appendContextPath(options.data.contextPath, options.name);
options = {data: data};
data.contextPath = appendContextPath(
options.data.contextPath,
options.name
);
options = { data: data };
}
return fn(context, options);
+37 -25
View File
@@ -1,4 +1,10 @@
import {appendContextPath, blockParams, createFrame, isArray, isFunction} from '../utils';
import {
appendContextPath,
blockParams,
createFrame,
isArray,
isFunction
} from '../utils';
import Exception from '../exception';
export default function(instance) {
@@ -8,17 +14,20 @@ export default function(instance) {
}
let fn = options.fn,
inverse = options.inverse,
i = 0,
ret = '',
data,
contextPath;
inverse = options.inverse,
i = 0,
ret = '',
data,
contextPath;
if (options.data && options.ids) {
contextPath = appendContextPath(options.data.contextPath, options.ids[0]) + '.';
contextPath =
appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (isFunction(context)) { context = context.call(this); }
if (isFunction(context)) {
context = context.call(this);
}
if (options.data) {
data = createFrame(options.data);
@@ -36,10 +45,15 @@ export default function(instance) {
}
}
ret = ret + fn(context[field], {
data: data,
blockParams: blockParams([context[field], field], [contextPath + field, null])
});
ret =
ret +
fn(context[field], {
data: data,
blockParams: blockParams(
[context[field], field],
[contextPath + field, null]
)
});
}
if (context && typeof context === 'object') {
@@ -49,9 +63,9 @@ export default function(instance) {
execIteration(i, i, i === context.length - 1);
}
}
} else if (global.Symbol && context[global.Symbol.iterator]) {
} else if (typeof Symbol === 'function' && context[Symbol.iterator]) {
const newContext = [];
const iterator = context[global.Symbol.iterator]();
const iterator = context[Symbol.iterator]();
for (let it = iterator.next(); !it.done; it = iterator.next()) {
newContext.push(it.value);
}
@@ -62,18 +76,16 @@ export default function(instance) {
} else {
let priorKey;
for (let key in context) {
if (context.hasOwnProperty(key)) {
// We're running the iterations one step out of sync so we can detect
// the last iteration without have to scan the object twice and create
// an itermediate keys array.
if (priorKey !== undefined) {
execIteration(priorKey, i - 1);
}
priorKey = key;
i++;
Object.keys(context).forEach(key => {
// We're running the iterations one step out of sync so we can detect
// the last iteration without have to scan the object twice and create
// an itermediate keys array.
if (priorKey !== undefined) {
execIteration(priorKey, i - 1);
}
}
priorKey = key;
i++;
});
if (priorKey !== undefined) {
execIteration(priorKey, i - 1, true);
}
+3 -1
View File
@@ -7,7 +7,9 @@ export default function(instance) {
return undefined;
} else {
// Someone is actually trying to call something, blow up.
throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"');
throw new Exception(
'Missing helper: "' + arguments[arguments.length - 1].name + '"'
);
}
});
}
+14 -4
View File
@@ -3,8 +3,12 @@ import Exception from '../exception';
export default function(instance) {
instance.registerHelper('if', function(conditional, options) {
if (arguments.length != 2) { throw new Exception('#if requires exactly one argument');}
if (isFunction(conditional)) { conditional = conditional.call(this); }
if (arguments.length != 2) {
throw new Exception('#if requires exactly one argument');
}
if (isFunction(conditional)) {
conditional = conditional.call(this);
}
// Default behavior is to render the positive path if the value is truthy and not empty.
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
@@ -17,7 +21,13 @@ export default function(instance) {
});
instance.registerHelper('unless', function(conditional, options) {
if (arguments.length != 2) { throw new Exception('#unless requires exactly one argument');}
return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
if (arguments.length != 2) {
throw new Exception('#unless requires exactly one argument');
}
return instance.helpers['if'].call(this, conditional, {
fn: options.inverse,
inverse: options.fn,
hash: options.hash
});
});
}
+2 -2
View File
@@ -1,7 +1,7 @@
export default function(instance) {
instance.registerHelper('log', function(/* message, options */) {
let args = [undefined],
options = arguments[arguments.length - 1];
options = arguments[arguments.length - 1];
for (let i = 0; i < arguments.length - 1; i++) {
args.push(arguments[i]);
}
@@ -14,6 +14,6 @@ export default function(instance) {
}
args[0] = level;
instance.log(... args);
instance.log(...args);
});
}
+3 -5
View File
@@ -1,11 +1,9 @@
export default function(instance) {
instance.registerHelper('lookup', function(obj, field) {
instance.registerHelper('lookup', function(obj, field, options) {
if (!obj) {
// Note for 5.0: Change to "obj == null" in 5.0
return obj;
}
if (field === 'constructor' && !obj.propertyIsEnumerable(field)) {
return undefined;
}
return obj[field];
return options.lookupProperty(obj, field);
});
}
+17 -4
View File
@@ -1,10 +1,20 @@
import { appendContextPath, blockParams, createFrame, isEmpty, isFunction } from '../utils';
import {
appendContextPath,
blockParams,
createFrame,
isEmpty,
isFunction
} from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('with', function(context, options) {
if (arguments.length != 2) { throw new Exception('#with requires exactly one argument');}
if (isFunction(context)) { context = context.call(this); }
if (arguments.length != 2) {
throw new Exception('#with requires exactly one argument');
}
if (isFunction(context)) {
context = context.call(this);
}
let fn = options.fn;
@@ -12,7 +22,10 @@ export default function(instance) {
let data = options.data;
if (options.data && options.ids) {
data = createFrame(options.data);
data.contextPath = appendContextPath(options.data.contextPath, options.ids[0]);
data.contextPath = appendContextPath(
options.data.contextPath,
options.ids[0]
);
}
return fn(context, {
@@ -0,0 +1,11 @@
import { extend } from '../utils';
/**
* Create a new object with "null"-prototype to avoid truthy results on prototype properties.
* The resulting object can be used with "object[property]" to check if a property exists
* @param {...object} sources a varargs parameter of source objects that will be merged
* @returns {object}
*/
export function createNewLookupObject(...sources) {
return extend(Object.create(null), ...sources);
}
+70
View File
@@ -0,0 +1,70 @@
import { createNewLookupObject } from './create-new-lookup-object';
import logger from '../logger';
const loggedProperties = Object.create(null);
export function createProtoAccessControl(runtimeOptions) {
let defaultMethodWhiteList = Object.create(null);
defaultMethodWhiteList['constructor'] = false;
defaultMethodWhiteList['__defineGetter__'] = false;
defaultMethodWhiteList['__defineSetter__'] = false;
defaultMethodWhiteList['__lookupGetter__'] = false;
let defaultPropertyWhiteList = Object.create(null);
// eslint-disable-next-line no-proto
defaultPropertyWhiteList['__proto__'] = false;
return {
properties: {
whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
},
methods: {
whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
};
}
export function resultIsAllowed(result, protoAccessControl, propertyName) {
if (typeof result === 'function') {
return checkWhiteList(protoAccessControl.methods, propertyName);
} else {
return checkWhiteList(protoAccessControl.properties, propertyName);
}
}
function checkWhiteList(protoAccessControlForType, propertyName) {
if (protoAccessControlForType.whitelist[propertyName] !== undefined) {
return protoAccessControlForType.whitelist[propertyName] === true;
}
if (protoAccessControlForType.defaultValue !== undefined) {
return protoAccessControlForType.defaultValue;
}
logUnexpecedPropertyAccessOnce(propertyName);
return false;
}
function logUnexpecedPropertyAccessOnce(propertyName) {
if (loggedProperties[propertyName] !== true) {
loggedProperties[propertyName] = true;
logger.log(
'error',
`Handlebars: Access has been denied to resolve the property "${propertyName}" because it is not an "own property" of its parent.\n` +
`You can add a runtime option to disable the check or this warning:\n` +
`See https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access for details`
);
}
}
export function resetLoggedProperties() {
Object.keys(loggedProperties).forEach(propertyName => {
delete loggedProperties[propertyName];
});
}
+13
View File
@@ -0,0 +1,13 @@
export function wrapHelper(helper, transformOptionsFn) {
if (typeof helper !== 'function') {
// This should not happen, but apparently it does in https://github.com/wycats/handlebars.js/issues/1639
// We try to make the wrapper least-invasive by not wrapping it, if the helper is not a function.
return helper;
}
let wrapper = function(/* dynamic arguments */) {
const options = arguments[arguments.length - 1];
arguments[arguments.length - 1] = transformOptionsFn(options);
return helper.apply(this, arguments);
};
return wrapper;
}
+7 -3
View File
@@ -1,4 +1,4 @@
import {indexOf} from './utils';
import { indexOf } from './utils';
let logger = {
methodMap: ['debug', 'info', 'warn', 'error'],
@@ -22,9 +22,13 @@ let logger = {
log: function(level, ...message) {
level = logger.lookupLevel(level);
if (typeof console !== 'undefined' && logger.lookupLevel(logger.level) <= level) {
if (
typeof console !== 'undefined' &&
logger.lookupLevel(logger.level) <= level
) {
let method = logger.methodMap[level];
if (!console[method]) { // eslint-disable-line no-console
// eslint-disable-next-line no-console
if (!console[method]) {
method = 'log';
}
console[method](...message); // eslint-disable-line no-console
+15 -5
View File
@@ -1,12 +1,22 @@
/* global window */
/* global globalThis */
export default function(Handlebars) {
/* istanbul ignore next */
let root = typeof global !== 'undefined' ? global : window,
$Handlebars = root.Handlebars;
// https://mathiasbynens.be/notes/globalthis
(function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // eslint-disable-line no-undef
delete Object.prototype.__magic__;
})();
const $Handlebars = globalThis.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars;
}
return Handlebars;
};
+203 -46
View File
@@ -1,30 +1,52 @@
import * as Utils from './utils';
import Exception from './exception';
import {COMPILER_REVISION, createFrame, LAST_COMPATIBLE_COMPILER_REVISION, REVISION_CHANGES} from './base';
import {moveHelperToHooks} from './helpers';
import {
COMPILER_REVISION,
createFrame,
LAST_COMPATIBLE_COMPILER_REVISION,
REVISION_CHANGES
} from './base';
import { moveHelperToHooks } from './helpers';
import { wrapHelper } from './internal/wrapHelper';
import {
createProtoAccessControl,
resultIsAllowed
} from './internal/proto-access';
export function checkRevision(compilerInfo) {
const compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
const compilerRevision = (compilerInfo && compilerInfo[0]) || 1,
currentRevision = COMPILER_REVISION;
if (compilerRevision >= LAST_COMPATIBLE_COMPILER_REVISION && compilerRevision <= COMPILER_REVISION) {
if (
compilerRevision >= LAST_COMPATIBLE_COMPILER_REVISION &&
compilerRevision <= COMPILER_REVISION
) {
return;
}
if (compilerRevision < LAST_COMPATIBLE_COMPILER_REVISION) {
const runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception('Template was precompiled with an older version of Handlebars than the current runtime. ' +
'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').');
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception(
'Template was precompiled with an older version of Handlebars than the current runtime. ' +
'Please update your precompiler to a newer version (' +
runtimeVersions +
') or downgrade your runtime to an older version (' +
compilerVersions +
').'
);
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw new Exception('Template was precompiled with a newer version of Handlebars than the current runtime. ' +
'Please update your runtime to a newer version (' + compilerInfo[1] + ').');
throw new Exception(
'Template was precompiled with a newer version of Handlebars than the current runtime. ' +
'Please update your runtime to a newer version (' +
compilerInfo[1] +
').'
);
}
}
export function template(templateSpec, env) {
/* istanbul ignore next */
if (!env) {
throw new Exception('No environment passed to template');
@@ -40,7 +62,8 @@ export function template(templateSpec, env) {
env.VM.checkRevision(templateSpec.compiler);
// backwards compatibility for precompiled templates with compiler-version 7 (<4.3.0)
const templateWasPrecompiledWithCompilerV7 = templateSpec.compiler && templateSpec.compiler[0] === 7;
const templateWasPrecompiledWithCompilerV7 =
templateSpec.compiler && templateSpec.compiler[0] === 7;
function invokePartialWrapper(partial, context, options) {
if (options.hash) {
@@ -51,13 +74,25 @@ export function template(templateSpec, env) {
}
partial = env.VM.resolvePartial.call(this, partial, context, options);
let optionsWithHooks = Utils.extend({}, options, {hooks: this.hooks});
let extendedOptions = Utils.extend({}, options, {
hooks: this.hooks,
protoAccessControl: this.protoAccessControl
});
let result = env.VM.invokePartial.call(this, partial, context, optionsWithHooks);
let result = env.VM.invokePartial.call(
this,
partial,
context,
extendedOptions
);
if (result == null && env.compile) {
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
result = options.partials[options.name](context, optionsWithHooks);
options.partials[options.name] = env.compile(
partial,
templateSpec.compilerOptions,
env
);
result = options.partials[options.name](context, extendedOptions);
}
if (result != null) {
if (options.indent) {
@@ -73,7 +108,11 @@ export function template(templateSpec, env) {
}
return result;
} else {
throw new Exception('The partial ' + options.name + ' could not be compiled when running in runtime-only mode');
throw new Exception(
'The partial ' +
options.name +
' could not be compiled when running in runtime-only mode'
);
}
}
@@ -81,14 +120,31 @@ export function template(templateSpec, env) {
let container = {
strict: function(obj, name, loc) {
if (!obj || !(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj, { loc: loc });
throw new Exception('"' + name + '" not defined in ' + obj, {
loc: loc
});
}
return obj[name];
return container.lookupProperty(obj, name);
},
lookupProperty: function(parent, propertyName) {
let result = parent[propertyName];
if (result == null) {
return result;
}
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return result;
}
if (resultIsAllowed(result, container.protoAccessControl, propertyName)) {
return result;
}
return undefined;
},
lookup: function(depths, name) {
const len = depths.length;
for (let i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
let result = depths[i] && container.lookupProperty(depths[i], name);
if (result != null) {
return depths[i][name];
}
}
@@ -109,9 +165,17 @@ export function template(templateSpec, env) {
programs: [],
program: function(i, data, declaredBlockParams, blockParams, depths) {
let programWrapper = this.programs[i],
fn = this.fn(i);
fn = this.fn(i);
if (data || depths || blockParams || declaredBlockParams) {
programWrapper = wrapProgram(this, i, fn, data, declaredBlockParams, blockParams, depths);
programWrapper = wrapProgram(
this,
i,
fn,
data,
declaredBlockParams,
blockParams,
depths
);
} else if (!programWrapper) {
programWrapper = this.programs[i] = wrapProgram(this, i, fn);
}
@@ -124,6 +188,15 @@ export function template(templateSpec, env) {
}
return value;
},
mergeIfNeeded: function(param, common) {
let obj = param || common;
if (param && common && param !== common) {
obj = Utils.extend({}, common, param);
}
return obj;
},
// An empty object to use as replacement for null-contexts
nullContext: Object.seal({}),
@@ -139,47 +212,82 @@ export function template(templateSpec, env) {
data = initData(context, data);
}
let depths,
blockParams = templateSpec.useBlockParams ? [] : undefined;
blockParams = templateSpec.useBlockParams ? [] : undefined;
if (templateSpec.useDepths) {
if (options.depths) {
depths = context != options.depths[0] ? [context].concat(options.depths) : options.depths;
depths =
context != options.depths[0]
? [context].concat(options.depths)
: options.depths;
} else {
depths = [context];
}
}
function main(context/*, options*/) {
return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths);
function main(context /*, options*/) {
return (
'' +
templateSpec.main(
container,
context,
container.helpers,
container.partials,
data,
blockParams,
depths
)
);
}
main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams);
main = executeDecorators(
templateSpec.main,
main,
container,
options.depths || [],
data,
blockParams
);
return main(context, options);
}
ret.isTop = true;
ret._setup = function(options) {
if (!options.partial) {
container.helpers = Utils.extend({}, env.helpers, options.helpers);
let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
wrapHelpersToPassLookupProperty(mergedHelpers, container);
container.helpers = mergedHelpers;
if (templateSpec.usePartial) {
container.partials = Utils.extend({}, env.partials, options.partials);
// Use mergeIfNeeded here to prevent compiling global partials multiple times
container.partials = container.mergeIfNeeded(
options.partials,
env.partials
);
}
if (templateSpec.usePartial || templateSpec.useDecorators) {
container.decorators = Utils.extend({}, env.decorators, options.decorators);
container.decorators = Utils.extend(
{},
env.decorators,
options.decorators
);
}
container.hooks = {};
container.protoAccessControl = createProtoAccessControl(options);
let keepHelperInHelpers = options.allowCallsToHelperMissing || templateWasPrecompiledWithCompilerV7;
let keepHelperInHelpers =
options.allowCallsToHelperMissing ||
templateWasPrecompiledWithCompilerV7;
moveHelperToHooks(container, 'helperMissing', keepHelperInHelpers);
moveHelperToHooks(container, 'blockHelperMissing', keepHelperInHelpers);
} else {
container.protoAccessControl = options.protoAccessControl; // internal option
container.helpers = options.helpers;
container.partials = options.partials;
container.decorators = options.decorators;
container.hooks = options.hooks;
}
};
ret._child = function(i, data, blockParams, depths) {
@@ -190,24 +298,47 @@ export function template(templateSpec, env) {
throw new Exception('must pass parent depths');
}
return wrapProgram(container, i, templateSpec[i], data, 0, blockParams, depths);
return wrapProgram(
container,
i,
templateSpec[i],
data,
0,
blockParams,
depths
);
};
return ret;
}
export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) {
export function wrapProgram(
container,
i,
fn,
data,
declaredBlockParams,
blockParams,
depths
) {
function prog(context, options = {}) {
let currentDepths = depths;
if (depths && context != depths[0] && !(context === container.nullContext && depths[0] === null)) {
if (
depths &&
context != depths[0] &&
!(context === container.nullContext && depths[0] === null)
) {
currentDepths = [context].concat(depths);
}
return fn(container,
context,
container.helpers, container.partials,
options.data || data,
blockParams && [options.blockParams].concat(blockParams),
currentDepths);
return fn(
container,
context,
container.helpers,
container.partials,
options.data || data,
blockParams && [options.blockParams].concat(blockParams),
currentDepths
);
}
prog = executeDecorators(fn, prog, container, depths, data, blockParams);
@@ -249,8 +380,10 @@ export function invokePartial(partial, context, options) {
options.data = createFrame(options.data);
// Wrapper function to get access to currentPartialBlock from the closure
let fn = options.fn;
partialBlock = options.data['partial-block'] = function partialBlockWrapper(context, options = {}) {
partialBlock = options.data['partial-block'] = function partialBlockWrapper(
context,
options = {}
) {
// Restore the partial-block from the closure for the execution of the block
// i.e. the part inside the block of the partial call.
options.data = createFrame(options.data);
@@ -273,7 +406,9 @@ export function invokePartial(partial, context, options) {
}
}
export function noop() { return ''; }
export function noop() {
return '';
}
function initData(context, data) {
if (!data || !('root' in data)) {
@@ -286,8 +421,30 @@ function initData(context, data) {
function executeDecorators(fn, prog, container, depths, data, blockParams) {
if (fn.decorator) {
let props = {};
prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths);
prog = fn.decorator(
prog,
props,
container,
depths && depths[0],
data,
blockParams,
depths
);
Utils.extend(prog, props);
}
return prog;
}
function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
Object.keys(mergedHelpers).forEach(helperName => {
let helper = mergedHelpers[helperName];
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
});
}
function passLookupPropertyOption(helper, container) {
const lookupProperty = container.lookupProperty;
return wrapHelper(helper, options => {
return Utils.extend({ lookupProperty }, options);
});
}
+17 -10
View File
@@ -1,4 +1,3 @@
const escape = {
'&': '&amp;',
'<': '&lt;',
@@ -10,13 +9,13 @@ const escape = {
};
const badChars = /[&<>"'`=]/g,
possible = /[&<>"'`=]/;
possible = /[&<>"'`=]/;
function escapeChar(chr) {
return escape[chr];
}
export function extend(obj/* , ...source */) {
export function extend(obj /* , ...source */) {
for (let i = 1; i < arguments.length; i++) {
for (let key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
@@ -40,16 +39,23 @@ let isFunction = function(value) {
/* istanbul ignore next */
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
return (
typeof value === 'function' &&
toString.call(value) === '[object Function]'
);
};
}
export {isFunction};
export { isFunction };
/* eslint-enable func-style */
/* istanbul ignore next */
export const isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
export const isArray =
Array.isArray ||
function(value) {
return value && typeof value === 'object'
? toString.call(value) === '[object Array]'
: false;
};
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) {
@@ -61,7 +67,6 @@ export function indexOf(array, value) {
return -1;
}
export function escapeExpression(string) {
if (typeof string !== 'string') {
// don't escape SafeStrings, since they're already safe
@@ -79,7 +84,9 @@ export function escapeExpression(string) {
string = '' + string;
}
if (!possible.test(string)) { return string; }
if (!possible.test(string)) {
return string;
}
return string.replace(badChars, escapeChar);
}
+1
View File
@@ -1,5 +1,6 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-env node */
/* eslint-disable no-var */
// var local = handlebars.create();
+118 -75
View File
@@ -1,10 +1,10 @@
/* eslint-env node */
/* eslint-disable no-console */
import Async from 'neo-async';
import fs from 'fs';
import * as Handlebars from './handlebars';
import {basename} from 'path';
import {SourceMapConsumer, SourceNode} from 'source-map';
import { basename } from 'path';
import { SourceMapConsumer, SourceNode } from 'source-map';
module.exports.loadTemplates = function(opts, callback) {
loadStrings(opts, function(err, strings) {
@@ -25,14 +25,19 @@ module.exports.loadTemplates = function(opts, callback) {
function loadStrings(opts, callback) {
let strings = arrayCast(opts.string),
names = arrayCast(opts.name);
names = arrayCast(opts.name);
if (names.length !== strings.length
&& strings.length > 1) {
return callback(new Handlebars.Exception('Number of names did not match the number of string inputs'));
if (names.length !== strings.length && strings.length > 1) {
return callback(
new Handlebars.Exception(
'Number of names did not match the number of string inputs'
)
);
}
Async.map(strings, function(string, callback) {
Async.map(
strings,
function(string, callback) {
if (string !== '-') {
callback(undefined, string);
} else {
@@ -55,80 +60,94 @@ function loadStrings(opts, callback) {
source: string
}));
callback(err, strings);
});
}
);
}
function loadFiles(opts, callback) {
// Build file extension pattern
let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-[\]]/g, function(arg) { return '\\' + arg; });
let extension = (opts.extension || 'handlebars').replace(
/[\\^$*+?.():=!|{}\-[\]]/g,
function(arg) {
return '\\' + arg;
}
);
extension = new RegExp('\\.' + extension + '$');
let ret = [],
queue = (opts.files || []).map((template) => ({template, root: opts.root}));
Async.whilst(() => queue.length, function(callback) {
let {template: path, root} = queue.shift();
queue = (opts.files || []).map(template => ({ template, root: opts.root }));
Async.whilst(
() => queue.length,
function(callback) {
let { template: path, root } = queue.shift();
fs.stat(path, function(err, stat) {
if (err) {
return callback(new Handlebars.Exception(`Unable to open template file "${path}"`));
}
fs.stat(path, function(err, stat) {
if (err) {
return callback(
new Handlebars.Exception(`Unable to open template file "${path}"`)
);
}
if (stat.isDirectory()) {
opts.hasDirectory = true;
if (stat.isDirectory()) {
opts.hasDirectory = true;
fs.readdir(path, function(err, children) {
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
children.forEach(function(file) {
let childPath = path + '/' + file;
if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) {
queue.push({template: childPath, root: root || path});
fs.readdir(path, function(err, children) {
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
});
children.forEach(function(file) {
let childPath = path + '/' + file;
callback();
});
if (
extension.test(childPath) ||
fs.statSync(childPath).isDirectory()
) {
queue.push({ template: childPath, root: root || path });
}
});
callback();
});
} else {
fs.readFile(path, 'utf8', function(err, data) {
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
// Clean the template name
let name = path;
if (!root) {
name = basename(name);
} else if (name.indexOf(root) === 0) {
name = name.substring(root.length + 1);
}
name = name.replace(extension, '');
ret.push({
path: path,
name: name,
source: data
});
callback();
});
}
});
},
function(err) {
if (err) {
callback(err);
} else {
fs.readFile(path, 'utf8', function(err, data) {
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
// Clean the template name
let name = path;
if (!root) {
name = basename(name);
} else if (name.indexOf(root) === 0) {
name = name.substring(root.length + 1);
}
name = name.replace(extension, '');
ret.push({
path: path,
name: name,
source: data
});
callback();
});
callback(undefined, ret);
}
});
},
function(err) {
if (err) {
callback(err);
} else {
callback(undefined, ret);
}
});
);
}
module.exports.cli = function(opts) {
@@ -138,7 +157,9 @@ module.exports.cli = function(opts) {
}
if (!opts.templates.length && !opts.hasDirectory) {
throw new Handlebars.Exception('Must define at least one template or directory.');
throw new Handlebars.Exception(
'Must define at least one template or directory.'
);
}
if (opts.simple && opts.min) {
@@ -147,12 +168,18 @@ module.exports.cli = function(opts) {
const multiple = opts.templates.length !== 1 || opts.hasDirectory;
if (opts.simple && multiple) {
throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
throw new Handlebars.Exception(
'Unable to output multiple templates in simple mode'
);
}
// Force simple mode if we have only one template and it's unnamed.
if (!opts.amd && !opts.commonjs && opts.templates.length === 1
&& !opts.templates[0].name) {
if (
!opts.amd &&
!opts.commonjs &&
opts.templates.length === 1 &&
!opts.templates[0].name
) {
opts.simple = true;
}
@@ -172,7 +199,11 @@ module.exports.cli = function(opts) {
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
output.add(
"define(['" +
opts.handlebarPath +
'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
);
} else if (opts.commonjs) {
output.add('var Handlebars = require("' + opts.commonjs + '");');
} else {
@@ -206,7 +237,10 @@ module.exports.cli = function(opts) {
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer);
precompiled = SourceNode.fromStringWithSourceMap(
precompiled.code,
consumer
);
}
if (opts.simple) {
@@ -219,7 +253,14 @@ module.exports.cli = function(opts) {
if (opts.amd && !multiple) {
output.add('return ');
}
output.add([objectName, '[\'', template.name, '\'] = template(', precompiled, ');\n']);
output.add([
objectName,
"['",
template.name,
"'] = template(",
precompiled,
');\n'
]);
}
});
@@ -286,7 +327,9 @@ function minify(output, sourceMapFile) {
throw e;
}
// it does not exist!
console.error('Code minimization is disabled due to missing uglify-js dependency');
console.error(
'Code minimization is disabled due to missing uglify-js dependency'
);
return output;
}
return require('uglify-js').minify(output.code, {
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
'check-coverage': true,
branches: 100,
lines: 100,
functions: 100,
statements: 100,
exclude: ['**/spec/**', '**/handlebars/compiler/parser.js'],
reporter: 'html'
};
+21201 -2540
View File
File diff suppressed because it is too large Load Diff
+56 -21
View File
@@ -1,9 +1,9 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "4.5.1",
"version": "4.7.8",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"homepage": "https://handlebarsjs.com/",
"keywords": [
"handlebars",
"mustache",
@@ -12,7 +12,7 @@
],
"repository": {
"type": "git",
"url": "https://github.com/wycats/handlebars.js.git"
"url": "https://github.com/handlebars-lang/handlebars.js.git"
},
"author": "Yehuda Katz",
"license": "MIT",
@@ -21,27 +21,33 @@
"node": ">=0.4.7"
},
"dependencies": {
"neo-async": "^2.6.0",
"optimist": "^0.6.1",
"source-map": "^0.6.1"
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@knappi/grunt-saucelabs": "^9.0.2",
"@playwright/test": "^1.17.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
"chai": "^4.2.0",
"concurrently": "^5.0.0",
"dirty-chai": "^2.0.1",
"dtslint": "^0.5.5",
"dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3",
"eslint-plugin-compat": "^3.3.0",
"eslint": "^6.7.2",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-compat": "^3.13.0",
"eslint-plugin-es5": "^1.4.1",
"grunt": "^1.0.3",
"fs-extra": "^8.1.0",
"grunt": "^1.0.4",
"grunt-babel": "^5.0.0",
"grunt-bg-shell": "^2.3.3",
"grunt-cli": "^1",
"grunt-contrib-clean": "^1",
"grunt-contrib-concat": "^1",
@@ -50,14 +56,18 @@
"grunt-contrib-requirejs": "^1",
"grunt-contrib-uglify": "^1",
"grunt-contrib-watch": "^1.1.0",
"grunt-eslint": "^20.1.0",
"grunt-shell": "^4.0.0",
"grunt-webpack": "^1.0.8",
"istanbul": "^0.3.0",
"husky": "^3.1.0",
"jison": "~0.3.0",
"lint-staged": "^9.5.0",
"mocha": "^5",
"mock-stdin": "^0.3.0",
"mustache": "^2.1.3",
"nyc": "^14.1.1",
"prettier": "^1.19.1",
"semver": "^5.0.1",
"sinon": "^7.5.0",
"typescript": "^3.4.3",
"underscore": "^1.5.1",
"webpack": "^1.12.6",
@@ -65,16 +75,26 @@
},
"main": "lib/index.js",
"types": "types/index.d.ts",
"browser": {
".": "./dist/cjs/handlebars.js",
"./runtime": "./dist/cjs/handlebars.runtime.js"
},
"browser": "./dist/cjs/handlebars.js",
"bin": {
"handlebars": "bin/handlebars"
},
"scripts": {
"checkTypes": "dtslint types",
"test": "grunt"
"build": "grunt build",
"release": "npm run build && grunt release",
"format": "prettier --write '**/*.js' && eslint --fix .",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"lint:eslint": "eslint --max-warnings 0 .",
"lint:prettier": "prettier --check '**/*.js'",
"lint:types": "dtslint types",
"test": "npm run test:mocha",
"test:mocha": "grunt build && grunt test",
"test:browser": "playwright test --config tests/browser/playwright.config.js tests/browser/spec.js",
"test:integration": "grunt integration-tests",
"test:serve": "grunt connect:server:keepalive",
"extensive-tests-and-publish-to-aws": "npx mocha tasks/tests/ && grunt --stack extensive-tests-and-publish-to-aws",
"--- combined tasks ---": "",
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
},
"jspm": {
"main": "handlebars",
@@ -91,9 +111,24 @@
"dist/amd/**/*.js",
"dist/cjs/**/*.js",
"lib",
"print-script",
"release-notes.md",
"runtime.js",
"types/*.d.ts"
]
"types/*.d.ts",
"runtime.d.ts"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,css,json}": [
"prettier --write",
"git add"
],
"*.js": [
"eslint --fix",
"git add"
]
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true
};
+327 -50
View File
@@ -2,120 +2,351 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.1...master)
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...master)
## v4.7.8 - July 27th, 2023
- Make library compatible with workers (#1894) - 3d3796c
- Don't rely on Node.js global object (#1776) - 2954e7e
- Fix compiling of each block params in strict mode (#1855) - 30dbf04
- Fix rollup warning when importing Handlebars as ESM - 03d387b
- Fix bundler issue with webpack 5 (#1862) - c6c6bbb
- Use https instead of git for mustache submodule - 88ac068
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.7...v4.7.8)
## v4.7.7 - February 15th, 2021
- fix weird error in integration tests - eb860c0
- fix: check prototype property access in strict-mode (#1736) - b6d3de7
- fix: escape property names in compat mode (#1736) - f058970
- refactor: In spec tests, use expectTemplate over equals and shouldThrow (#1683) - 77825f8
- chore: start testing on Node.js 12 and 13 - 3789a30
(POSSIBLY) BREAKING CHANGES:
- the changes from version [4.6.0](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md#v460---january-8th-2020) now also apply
in when using the compile-option "strict: true". Access to prototype properties is forbidden completely by default, specific properties or methods
can be allowed via runtime-options. See #1633 for details. If you are using Handlebars as documented, you should not be accessing prototype properties
from your template anyway, so the changes should not be a problem for you. Only the use of undocumented features can break your build.
That is why we only bump the patch version despite mentioning breaking changes.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)
## v4.7.6 - April 3rd, 2020
Chore/Housekeeping:
- [#1672](https://github.com/wycats/handlebars.js/issues/1672) - Switch cmd parser to latest minimist ([@dougwilson](https://api.github.com/users/dougwilson)
Compatibility notes:
- Restored Node.js compatibility
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.5...v4.7.6)
## v4.7.5 - April 2nd, 2020
Chore/Housekeeping:
- ~Node.js version support has been changed to v6+~ Reverted in 4.7.6
Compatibility notes:
- ~Node.js < v6 is no longer supported~ Reverted in 4.7.6
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.4...v4.7.5)
## v4.7.4 - April 1st, 2020
Chore/Housekeeping:
- [#1666](https://github.com/wycats/handlebars.js/issues/1666) - Replaced minimist with yargs for handlebars CLI ([@aorinevo](https://api.github.com/users/aorinevo), [@AviVahl](https://api.github.com/users/AviVahl) & [@fabb](https://api.github.com/users/fabb))
Compatibility notes:
- No incompatibilities are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.3...v4.7.4)
## v4.7.3 - February 5th, 2020
Chore/Housekeeping:
- [#1644](https://github.com/wycats/handlebars.js/issues/1644) - Download links to aws broken on handlebarsjs.com - access denied ([@Tea56](https://api.github.com/users/Tea56))
- Fix spelling and punctuation in changelog - d78cc73
Bugfixes:
- Add Type Definition for Handlebars.VERSION, Fixes #1647 - 4de51fe
- Include Type Definition for runtime.js in Package - a32d05f
Compatibility notes:
- No incompatibilities are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.2...v4.7.3)
## v4.7.2 - January 13th, 2020
Bugfixes:
- fix: don't wrap helpers that are not functions - 9d5aa36, #1639
Chore/Build:
- chore: execute saucelabs-task only if access-key exists - a4fd391
Compatibility notes:
- No breaking changes are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.1...v4.7.2)
## v4.7.1 - January 12th, 2020
Bugfixes:
- fix: fix log output in case of illegal property access - f152dfc
- fix: log error for illegal property access only once per property - 3c1e252
Compatibility notes:
- no incompatibilities are to be expected.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.0...v4.7.1)
## v4.7.0 - January 10th, 2020
Features:
- feat: default options for controlling proto access - 7af1c12, #1635
- This makes it possible to disable the prototype access restrictions added in 4.6.0
- an error is logged in the console, if access to prototype properties is attempted and denied
and no explicit configuration has taken place.
Compatibility notes:
- no compatibilities are expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.6.0...v4.7.0)
## v4.6.0 - January 8th, 2020
Features:
- feat: access control to prototype properties via whitelist (#1633)- d03b6ec
Bugfixes:
- fix(runtime.js): partials compile not caching (#1600) - 23d58e7
Chores, docs:
- various refactorings and improvements to tests - d7f0dcf, 187d611, d337f40
- modernize the build-setup
- use prettier to format and eslint to verify - c40d9f3, 8901c28, e97685e, 1f61f21
- use nyc instead of istanbul to collect coverage - 164b7ff, 1ebce2b
- update build code to use modern javascript and make it cleaner - 14b621c, 1ec1737, 3a5b65e, dde108e, 04b1984, 587e7a3
- restructur build commands - e913dc5,
- eslint rule changes - ac4655e, dc54952
- Update (C) year in the LICENSE file - d1fb07b
- chore: try to fix saucelabs credentials (#1627) -
- Update readme.md with updated links (#1620) - edcc84f
BREAKING CHANGES:
- access to prototype properties is forbidden completely by default,
specific properties or methods can be allowed via runtime-options.
See #1633 for details.
If you are using Handlebars as documented, you should not be accessing prototype
properties from your template anyway, so the changes should not be a problem
for you. Only the use of undocumented features can break your build.
That is why we only bump the minor version despite mentioning breaking changes.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.3...v4.6.0)
## v4.5.3 - November 18th, 2019
Bugfixes:
- fix: add "no-prototype-builtins" eslint-rule and fix all occurences - f7f05d7
- fix: add more properties required to be enumerable - 1988878
Chores / Build:
- fix: use !== 0 instead of != 0 - c02b05f
- add chai and dirty-chai and sinon, for cleaner test-assertions and spies,
deprecate old assertion-methods - 93e284e, 886ba86, 0817dad, 93516a0
Security:
- The properties `__proto__`, `__defineGetter__`, `__defineSetter__` and `__lookupGetter__`
have been added to the list of "properties that must be enumerable".
If a property by that name is found and not enumerable on its parent,
it will silently evaluate to `undefined`. This is done in both the compiled template and the "lookup"-helper.
This will prevent new Remote-Code-Execution exploits that have been
published recently.
Compatibility notes:
- Due to the security-fixes. The semantics of the templates using
`__proto__`, `__defineGetter__`, `__defineSetter__` and `__lookupGetter__` in the respect that those expression now return
`undefined` rather than their actual value from the proto.
- The semantics have not changed in cases where the properties are enumerable, as in:
```js
{
__proto__: 'some string';
}
```
- The change may be breaking in that respect, but we still only
increase the patch-version, because the incompatible use-cases
are not intended, undocumented and far less important than fixing
Remote-Code-Execution exploits on existing systems.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.2...v4.5.3)
## v4.5.2 - November 13th, 2019
# Bugfixes
- fix: use String(field) in lookup when checking for "constructor" - d541378
- test: add fluent API for testing Handlebars - c2ac79c
Compatibility notes:
- no incompatibility are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.1...v4.5.2)
## v4.5.1 - October 29th, 2019
Bugfixs
- fix: move "eslint-plugin-compat" to devDependencies - 5e9d17f (#1589)
Compatibility notes:
- No compatibility issues are to be expected
- No compatibility issues are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.0...v4.5.1)
## v4.5.0 - October 28th, 2019
Features / Improvements
- Add method Handlebars.parseWithoutProcessing (#1584) - 62ed3c2
- add guard to if & unless helpers (#1549)
- show source location for the strict lookup exceptions - feb60f8
Bugfixes:
- Use objects for hash value tracking - 7fcf9d2
Chore:
- Resolve deprecation warning message from eslint while running eslint (#1586) - 7052e88
- chore: add eslint-plugin-compat and eslint-plugin-es5 - 088e618
Compatibility notes:
- No compatibility issues are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.4.5...v4.5.0)
## v4.4.5 - October 20th, 2019
Bugfixes:
Bugfixes:
- Contents of raw-blocks must be matched with non-eager regex-matching - 8d5530e, #1579
[Commits](https://github.com/wycats/handlebars.js/compare/v4.4.4...v4.4.5)
## v4.4.4 - October 20th, 2019
Bugfixes:
- fix: prevent zero length tokens in raw-blocks (#1577, #1578) - f1752fe
Chore:
Chore:
- chore: link to s3 bucket with https, add "npm ci" to build instructions - 0b593bf
Compatibility notes:
- no compatibility issues are expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.4.3...v4.4.4)
## v4.4.3 - October 8th, 2019
Bugfixes
Typings:
- add missing type fields to AST typings and add tests for them - 0440af2
[Commits](https://github.com/wycats/handlebars.js/compare/v4.4.2...v4.4.3)
## v4.4.2 - October 2nd, 2019
- chore: fix grunt-saucelabs dependency - b7eada0
- chore: fix grunt-saucelabs dependency - b7eada0
[Commits](https://github.com/wycats/handlebars.js/compare/v4.4.1...v4.4.2)
## v4.4.1 - October 2nd, 2019
- [#1562](https://github.com/wycats/handlebars.js/issues/1562) - Error message for syntax error missing location in 4.2.1+
[Commits](https://github.com/wycats/handlebars.js/compare/v4.4.0...v4.4.1)
## v4.4.0 - September 29th, 2019
- Added support for iterable objects in {{#each}} helper (#1557) - cf7545e
- Added support for iterable objects in {{#each}} helper (#1557) - cf7545e
[Commits](https://github.com/wycats/handlebars.js/compare/v4.3.4...v4.4.0)
## v4.3.4 - September 28th, 2019
- fix: harden "propertyIsEnumerable"-check - ff4d827
Compatibility notes:
- No incompatibilities are known.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.3.3...v4.3.4)
## v4.3.3 - September 27th, 2019
- fix test case for browsers that do not support __defineGetter__ - 8742bde
- fix test case for browsers that do not support **defineGetter** - 8742bde
[Commits](https://github.com/wycats/handlebars.js/compare/v4.3.2...v4.3.3)
## v4.3.2 - September 26th, 2019
- Use Object.prototype.propertyIsEnumerable to check for constructors - 213c0bb, #1563
Compatibility notes:
- There are no breaking changes
[Commits](https://github.com/wycats/handlebars.js/compare/v4.3.1...v4.3.2)
## v4.3.1 - September 25th, 2019
Fixes:
- do not break on precompiled templates from Handlebars >=4.0.0 <4.3.0 - 1266838, #1561
- Ensure allowCallsToHelperMissing runtime option is optional in typings - 93444c5, 64ecb9e, #1560
[Commits](https://github.com/wycats/handlebars.js/compare/v4.3.0...v4.3.1)
## v4.3.0 - September 24th, 2019
Fixes:
- Security: Disallow calling "helperMissing" and "blockHelperMissing" directly - 2078c72
@@ -128,18 +359,20 @@ Features:
Breaking changes:
Compatibility notes:
- Compiler revision increased - 06b7224
- This means that template compiled with versions prior to 4.3.0 will not work with runtimes >= 4.3.0
The increase was done because the "helperMissing" and "blockHelperMissing" are now moved from the helpers
to the internal "container.hooks" object, so old templates will not be able to call them anymore. We suggest
that you always recompile your templates with the latest compiler in your build pipelines.
- Disallow calling "helperMissing" and "blockHelperMissing" directly - 2078c72
- Calling "helperMissing" and "blockHelperMissing" directly from a template (like in `{{blockHelperMissing}}` was
never intended and was part of the exploits that have been revealed early in 2019
(see https://github.com/wycats/handlebars.js/issues/1495). *It is also part of a new exploit that
is not captured by the earlier fix.* In order to harden Handlebars against such exploits, calling thos helpers
is now not possible anymore. *Overriding* those helpers is still possible.
- Calling "helperMissing" and "blockHelperMissing" directly from a template (like in `{{blockHelperMissing}}` was
never intended and was part of the exploits that have been revealed early in 2019
(see https://github.com/wycats/handlebars.js/issues/1495). _It is also part of a new exploit that
is not captured by the earlier fix._ In order to harden Handlebars against such exploits, calling thos helpers
is now not possible anymore. _Overriding_ those helpers is still possible.
- If you really need this behavior, you can set the runtime option `allowCallsToHelperMissing` to `true` and the
calls will again be possible
@@ -147,86 +380,97 @@ Both bullet points imly that Handlebars is not 100% percent compatible to 4.2.0,
We consider it more important to resolve a major security issue than to maintain 100% compatibility.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.2.1...v4.3.0)
## v4.2.1 - September 20th, 2019
Bugfixes:
Bugfixes:
- The "browser" property in the package.json has been updated to use the common-js builds instead of the minified UMD - c55a7be, #1553
Compatibility notes:
- No compatibility issues should arise
[Commits](https://github.com/wycats/handlebars.js/compare/v4.2.0...v4.2.1)
## v4.2.0 - September 3rd, 2019
Chore/Test:
- Use custom `grunt-saucelab` with current sauce-connect proxy - f119497
- Add framework for various integration tests - f9cce4d
- Add integration test for webpack - a57b682
Bugfixes:
- [#1544](https://github.com/wycats/handlebars.js/issues/1544) - Typescript types: `knownHelpers` doesnt allow for custom helpers ([@NickCis](https://api.github.com/users/NickCis))
- [#1534](https://github.com/wycats/handlebars.js/pull/1534) - Add typings for "Handlebars.VM.resolvePartial ([@AndrewLeedham](https://api.github.com/users/AndrewLeedham))
Features:
- [#1540](https://github.com/wycats/handlebars.js/pull/1540) - added "browser"-property to package.json, resolves #1102 ([@ouijan](https://api.github.com/users/ouijan))
Compatibility notes:
- The new "browser"-property should not break anything, but you can never be sure. The integration test for webpack
- The new "browser"-property should not break anything, but you can never be sure. The integration test for webpack
shows that it works, but if it doesn't please open an issue.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2-0...v4.2.0)
## v4.1.2-0 - August 25th, 2019
[#1540](https://github.com/wycats/handlebars.js/pull/1540) - added browser to package.json, resolves #1102 ([@ouijan](https://api.github.com/users/ouijan))
Compatibility notes:
- We are not sure if imports via webpack are still working, which is why this release is a pre-release
[Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2...v4.1.2-0)
## v4.1.2 - April 13th, 2019
Chore/Test:
- [#1515](https://github.com/wycats/handlebars.js/pull/1515) - Port over linting and test for typings ([@zimmi88](https://api.github.com/users/zimmi88))
- chore: add missing typescript dependency, add package-lock.json - 594f1e3
- test: remove safari from saucelabs - 871accc
Bugfixes:
Bugfixes:
- fix: prevent RCE through the "lookup"-helper - cd38583
Compatibility notes:
Access to the constructor of a class thought `{{lookup obj "constructor" }}` is now prohibited. This closes
Access to the constructor of a class thought `{{lookup obj "constructor" }}` is now prohibited. This closes
a leak that only half closed in versions 4.0.13 and 4.1.0, but it is a slight incompatibility.
This kind of access is not the intended use of Handlebars and leads to the vulnerability described
in #1495. We will **not** increase the major version, because such use is not intended or documented,
in #1495. We will **not** increase the major version, because such use is not intended or documented,
and because of the potential impact of the issue (we fear that most people won't use a new major version
and the issue may not be resolved on many systems).
and the issue may not be resolved on many systems).
[Commits](https://github.com/wycats/handlebars.js/compare/v4.1.1...v4.1.2)
## v4.1.1 - March 16th, 2019
Bugfixes:
- fix: add "runtime.d.ts" to allow "require('handlebars/runtime')" in TypeScript - 5cedd62
Refactorings:
- replace "async" with "neo-async" - 048f2ce
- use "substring"-function instead of "substr" - 445ae12
Compatibility notes:
- This is a bugfix release. There are no breaking change and no new features.
- This is a bugfix release. There are no breaking change and no new features.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.1.0...v4.1.1)
## v4.1.0 - February 7th, 2019
New Features
- import TypeScript typings - 27ac1ee
@@ -259,11 +503,10 @@ document.getElementById('output').innerHTML = template(new SomeClass());
This kind of access is not the intended use of Handlebars and leads to the vulnerability described in #1495. We will **not** increase the major version, because such use is not intended or documented, and because of the potential impact of the issue (we fear that most people won't use a new major version and the issue may not be resolved on many systems).
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.12...v4.1.0)
## v4.0.12 - September 4th, 2018
New features:
- none
@@ -288,25 +531,27 @@ Removed obsolete code:
- Update jsfiddle to 4.0.11 - 8947dd0
Compatibility notes:
- No compatibility issues are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.11...v4.0.12)
## v4.0.11 - October 17th, 2017
- [#1391](https://github.com/wycats/handlebars.js/issues/1391) - `uglify-js` is unconditionally imported, but only listed as optional dependency ([@Turbo87](https://github.com/Turbo87))
- [#1233](https://github.com/wycats/handlebars.js/issues/1233) - Unable to build under windows - error at test:bin task ([@blikblum](https://github.com/blikblum))
- Update (C) year in the LICENSE file - 21386b6
Compatibility notes:
- This is a bugfix release. There are no breaking change and no new features.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.10...v4.0.11)
## v4.0.10 - May 21st, 2017
- Fix regression in 4.0.9: Replace "Object.assign" (not support in IE) by "util/extend" - 0e953d1
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.9...v4.0.10)
## v4.0.9 - May 21st, 2017
@@ -314,39 +559,44 @@ Compatibility notes:
- [#1327](https://github.com/wycats/handlebars.js/issues/1327) Handlebars.compile() does not modify "options" anymore
- pending [#1331](https://github.com/wycats/handlebars.js/issues/1331) Attempts to build Handlebars in a Windows environment
- Fix build in windows - cc554a5
- Ensure LF line-edings in handlebars-template fixtures (*.hbs) - ed879a6
- Ensure LF line-edings in handlebars-template fixtures (\*.hbs) - ed879a6
- Run integration test with `node handlebars -a ...` on Windows - 2e21e2b
- Ensure LF line-edings in lexer-files (*.l) - bdfdbea
- Ensure LF line-edings in lexer-files (\*.l) - bdfdbea
- Force LF line-endings for spec/artifacts - b50ef03
- Use istanbul/lib/cli.js instead of node_modules/.bin/istanbul - 6e6269f
- TravisCI: Publish valid semver tags independently of the branch - 7378f85
Compatibility notes:
- No compatibility issues are expected.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.8...v4.0.9)
## v4.0.8 - May 2nd, 2017
- [#1341](https://github.com/wycats/handlebars.js/issues/1341) [#1342](https://github.com/wycats/handlebars.js/issues/1342) Allow partial-blocks to be executed without "options" ([@nknapp](https://github.com/nknapp)) - a00c598
Compatibility notes:
- No breaking changes
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.7...v4.0.8)
## v4.0.7 - April 29th, 2017
- [#1319](https://github.com/wycats/handlebars.js/issues/1319): Fix context-stack when calling block-helpers on null values ([@nknapp](https://github.com/nknapp)) - c8f4b57
- [#1315](https://github.com/wycats/handlebars.js/pull/1315) Parser: Change suffix to use ES6 default module export ([@Turbo87](https://github.com/Turbo87))- b617375
- [#1290](https://github.com/wycats/handlebars.js/pull/1290) [#1252](https://github.com/wycats/handlebars.js/issue/1290) Add more tests for partial-blocks and inline partials ([@nknapp](https://github.com/nknapp)) - 63a8e0c
- [#1290](https://github.com/wycats/handlebars.js/pull/1290) [#1252](https://github.com/wycats/handlebars.js/issue/1290) Add more tests for partial-blocks and inline partials ([@nknapp](https://github.com/nknapp)) - 63a8e0c
- [#1252](https://github.com/wycats/handlebars.js/issue/1290) Using @partial-block twice in a template not possible ([@nknapp](https://github.com/nknapp)) - 5a164d0
- [#1310](https://github.com/wycats/handlebars.js/pull/1310) Avoid duplicate "sourceMappingURL=" lines. ([@joonas-lahtinen](https://github.com/joonas-lahtinen)) - 01b0f65
- [#1275](https://github.com/wycats/handlebars.js/pull/1275) require('sys') is deprecated, using 'util' instead ([@travnels](https://github.com/travnels)) - 406f2ee
- [#1285](https://github.com/wycats/handlebars.js/pull/1285) [#1284](https://github.com/wycats/handlebars.js/issues/1284) Make "column"-property of Errors enumerable ([@nknapp](https://github.com/nknapp)) - a023cb4
- [#1285](https://github.com/wycats/handlebars.js/pull/1285) Testcase to verify that compile-errors have a column-property ([@nknapp](https://github.com/nknapp)) - c7dc353
[Commits](https://github.com/lawnsea/handlebars.js/compare/v4.0.6...v4.0.7)
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.6...v4.0.7)
## v4.0.6 - November 12th, 2016
- [#1243](https://github.com/wycats/handlebars.js/pull/1243) - Walk up data frames for nested @partial-block ([@lawnsea](https://github.com/lawnsea))
- [#1210](https://github.com/wycats/handlebars.js/pull/1210) - Add a new lightweight package based on handlebars in the README ([@kabirbaidhya](https://github.com/kabirbaidhya))
- [#1187](https://github.com/wycats/handlebars.js/pull/1187) - Ensure that existing blockParams and depths are respected on dupe programs ([@charleso](https://github.com/charleso))
@@ -354,7 +604,7 @@ Compatibility notes:
- [#1177](https://github.com/wycats/handlebars.js/pull/1177) - Preserve License info in Closure Compiler ([@gennadiylitvinyuk](https://github.com/gennadiylitvinyuk))
- [#1171](https://github.com/wycats/handlebars.js/pull/1171) - Contributing doc fix: failing thats -> failing tests ([@paulfalgout](https://github.com/paulfalgout))
- [#1166](https://github.com/wycats/handlebars.js/pull/1166) - Update license date ([@timwangdev](https://github.com/timwangdev))
- Update jsfiddle to point to latest - 959ee55 (originally dfc7554 by [@kpdecker](https://github.com/kpdecker))
- Update jsfiddle to point to latest - 959ee55 (originally dfc7554 by [@kpdecker](https://github.com/kpdecker))
- [#1163](https://github.com/wycats/handlebars.js/pull/1163) - Fix typos on decorators-api.md. ([@adjohnson916](https://github.com/adjohnson916))
- Drop extra Error params - 8c19874 (originally 63fdb92 by [@kpdecker](https://github.com/kpdecker))
- [#1153](https://github.com/wycats/handlebars.js/pull/1153) - Add documentation for running tests to contributing.md ([@ryanmurakami](https://github.com/ryanmurakami))
@@ -368,6 +618,7 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.5...v4.0.6)
## v4.0.5 - November 19th, 2015
- [#1132](https://github.com/wycats/handlebars.js/pull/1132) - Update uglify-js to avoid vulnerability ([@plynchnlm](https://github.com/plynchnlm))
- [#1129](https://github.com/wycats/handlebars.js/issues/1129) - Minified lib returns an empty string ([@bricss](https://github.com/bricss))
- Return current handlebars instance from noConflict - 685cf92
@@ -380,12 +631,14 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.4...v4.0.5)
## v4.0.4 - October 29th, 2015
- [#1121](https://github.com/wycats/handlebars.js/pull/1121) - Include partial name in 'undefined partial' exception message ([@shinypb](https://github.com/shinypb))
- [#1125](https://github.com/wycats/handlebars.js/pull/1125) - Add promised-handlebars to "in-the-wild"-list ([@nknapp](https://github.com/nknapp))
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.3...v4.0.4)
## v4.0.3 - September 23rd, 2015
- [#1099](https://github.com/wycats/handlebars.js/issues/1099) - @partial-block is overridden ([@btmorex](https://github.com/btmorex))
- [#1093](https://github.com/wycats/handlebars.js/issues/1093) - #each skips iteration on undefined values ([@florianpilz](https://github.com/florianpilz))
- [#1092](https://github.com/wycats/handlebars.js/issues/1092) - Square braces in key name ([@distantnative](https://github.com/distantnative))
@@ -393,22 +646,26 @@ Compatibility notes:
- [#1090](https://github.com/wycats/handlebars.js/pull/1090) - grammar fixes in 4.0.0 release notes ([@nikolas](https://github.com/nikolas))
Compatibility notes:
- `each` iteration with `undefined` values has been restored to the 3.0 behaviors. Helper calls with undefined context values will now execute against an arbitrary empty object to avoid executing against global object in non-strict mode.
- `]` can now be included in `[]` wrapped identifiers by escaping with `\`. Any `[]` identifiers that include `\` will now have to properly escape these values.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.2...v4.0.3)
## v4.0.2 - September 4th, 2015
- [#1089](https://github.com/wycats/handlebars.js/issues/1089) - "Failover content" not working in multiple levels of inline partials ([@michaellopez](https://github.com/michaellopez))
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.1...v4.0.2)
## v4.0.1 - September 2nd, 2015
- Fix failure when using decorators in partials - 05b82a2
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...v4.0.1)
## v4.0.0 - September 1st, 2015
- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://github.com/kpdecker))
- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://github.com/kpdecker))
- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://github.com/denniskuczynski))
@@ -444,6 +701,7 @@ Compatibility notes:
- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa)
Compatibility notes:
- Depthed paths are now conditionally pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior that better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028).
- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `<div foo={{bar}}>`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces.
- AST constructors have been dropped in favor of plain old javascript objects
@@ -452,22 +710,25 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v4.0.0)
## v3.0.3 - April 28th, 2015
- [#1004](https://github.com/wycats/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://github.com/boskee))
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.2...v3.0.3)
## v3.0.2 - April 20th, 2015
- [#998](https://github.com/wycats/handlebars.js/pull/998) - Add full support for es6 ([@kpdecker](https://github.com/kpdecker))
- [#994](https://github.com/wycats/handlebars.js/issues/994) - Access Handlebars.Visitor in browser ([@tamlyn](https://github.com/tamlyn))
- [#990](https://github.com/wycats/handlebars.js/issues/990) - Allow passing null/undefined literals subexpressions ([@blimmer](https://github.com/blimmer))
- [#989](https://github.com/wycats/handlebars.js/issues/989) - Source-map error with requirejs ([@SteppeEagle](https://github.com/SteppeEagle))
- [#967](https://github.com/wycats/handlebars.js/issues/967) - can't access "this" property ([@75lb](https://github.com/75lb))
- [#967](https://github.com/wycats/handlebars.js/issues/967) - can't access "this" property ([@75lb](https://github.com/75lb))
- Use captureStackTrace for error handler - a009a97
- Ignore branches tested without coverage monitoring - 37a664b
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.1...v3.0.2)
## v3.0.1 - March 24th, 2015
- [#984](https://github.com/wycats/handlebars.js/pull/984) - Adding documentation for passing arguments into partials ([@johneke](https://github.com/johneke))
- [#973](https://github.com/wycats/handlebars.js/issues/973) - version 3 is slower than version 2 ([@elover](https://github.com/elover))
- [#966](https://github.com/wycats/handlebars.js/issues/966) - "handlebars --version" does not work with v3.0.0 ([@abloomston](https://github.com/abloomston))
@@ -477,6 +738,7 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.0...v3.0.1)
## v3.0.0 - February 10th, 2015
- [#941](https://github.com/wycats/handlebars.js/pull/941) - Add support for dynamic partial names ([@kpdecker](https://github.com/kpdecker))
- [#940](https://github.com/wycats/handlebars.js/pull/940) - Add missing reserved words so compiler knows to use array syntax: ([@mattflaschen](https://github.com/mattflaschen))
- [#938](https://github.com/wycats/handlebars.js/pull/938) - Fix example using #with helper ([@diwo](https://github.com/diwo))
@@ -506,6 +768,7 @@ Compatibility notes:
- Lookup partials when undefined - 617dd57
Compatibility notes:
- Runtime breaking changes. Must match 3.x runtime and precompiler.
- The AST has been upgraded to a public API.
- There are a number of changes to this, but the format is now documented in docs/compiler-api.md
@@ -515,6 +778,7 @@ Compatibility notes:
- `SafeString` is now duck typed on `toHTML`
New Features:
- noConflict
- Source Maps
- Block Params
@@ -527,6 +791,7 @@ New Features:
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...v3.0.0)
## v2.0.0 - September 1st, 2014
- Update jsfiddle to 2.0.0-beta.1 - 0670f65
- Add contrib note regarding handlebarsjs.com docs - 4d17e3c
- Play nice with gemspec version numbers - 64d5481
@@ -534,6 +799,7 @@ New Features:
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-beta.1...v2.0.0)
## v2.0.0-beta.1 - August 26th, 2014
- [#787](https://github.com/wycats/handlebars.js/pull/787) - Remove whitespace surrounding standalone statements ([@kpdecker](https://github.com/kpdecker))
- [#827](https://github.com/wycats/handlebars.js/issues/827) - Render false literal as “false” ([@scoot557](https://github.com/scoot557))
- [#767](https://github.com/wycats/handlebars.js/issues/767) - Subexpressions bug with hash and context ([@evensoul](https://github.com/evensoul))
@@ -543,7 +809,7 @@ New Features:
- [#837](https://github.com/wycats/handlebars.js/issues/837) - Strange input for custom helper ( foo.bar == false when foo is undefined ) ([@zordius](https://github.com/zordius))
- [#819](https://github.com/wycats/handlebars.js/pull/819) - Implement recursive field lookup ([@kpdecker](https://github.com/kpdecker))
- [#764](https://github.com/wycats/handlebars.js/issues/764) - This reference not working for helpers ([@kpdecker](https://github.com/kpdecker))
- [#773](https://github.com/wycats/handlebars.js/issues/773) - Implicit parameters in {{#each}} introduces a peculiarity in helpers calling convention ([@Bertrand](https://github.com/Bertrand))
- [#773](https://github.com/wycats/handlebars.js/issues/773) - Implicit parameters in {{#each}} introduces a peculiarity in helpers calling convention ([@Bertrand](https://github.com/Bertrand))
- [#783](https://github.com/wycats/handlebars.js/issues/783) - helperMissing and consistency for different expression types ([@ErisDS](https://github.com/ErisDS))
- [#795](https://github.com/wycats/handlebars.js/pull/795) - Turn the precompile script into a wrapper around a module. ([@jwietelmann](https://github.com/jwietelmann))
- [#823](https://github.com/wycats/handlebars.js/pull/823) - Support inverse sections on the with helper ([@dan-manges](https://github.com/dan-manges))
@@ -556,13 +822,14 @@ New Features:
- Fix compiler program de-duping - 9e3f824
Compatibility notes:
- The default build now outputs a generic UMD wrapper. This should be transparent change but may cause issues in some environments.
- Runtime compatibility breaks in both directions. Ensure that both compiler and client are upgraded to 2.0.0-beta.1 or higher at the same time.
- `programWithDepth` has been removed an instead an array of context values is passed to fields needing depth lookups.
- `false` values are now printed to output rather than silently dropped
- Lines containing only block statements and whitespace are now removed. This matches the Mustache spec but may cause issues with code that expects whitespace to exist but would not otherwise.
- Partials that are standalone will now indent their rendered content
- `AST.ProgramNode`'s signature has changed.
- `AST.ProgramNode`'s signature has changed.
- Numerious methods/features removed from pseudo-API classes
- `JavaScriptCompiler.register`
- `JavaScriptCompiler.replaceStack` no longer supports non-inline replace
@@ -577,11 +844,13 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.4...v2.0.0-beta.1)
## v2.0.0-alpha.4 - May 19th, 2014
- Expose setup wrappers for compiled templates - 3638874
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.3...v2.0.0-alpha.4)
## v2.0.0-alpha.3 - May 19th, 2014
- [#797](https://github.com/wycats/handlebars.js/pull/797) - Pass full helper ID to helperMissing when options are provided ([@tomdale](https://github.com/tomdale))
- [#793](https://github.com/wycats/handlebars.js/pull/793) - Ensure isHelper is coerced to a boolean ([@mmun](https://github.com/mmun))
- Refactor template init logic - 085e5e1
@@ -589,6 +858,7 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.2...v2.0.0-alpha.3)
## v2.0.0-alpha.2 - March 6th, 2014
- [#756](https://github.com/wycats/handlebars.js/pull/756) - fix bug in IE<=8 (no Array::map), closes #751 ([@jenseng](https://github.com/jenseng))
- [#749](https://github.com/wycats/handlebars.js/pull/749) - properly handle multiple subexpressions in the same hash, fixes #748 ([@jenseng](https://github.com/jenseng))
- [#743](https://github.com/wycats/handlebars.js/issues/743) - subexpression confusion/problem? ([@waynedpj](https://github.com/waynedpj))
@@ -598,11 +868,13 @@ Compatibility notes:
- Temporarily kill the AWS publish CI step - 8347ee2
Compatibility notes:
- A JSON polyfill is required to run the compiler under IE8 and below. It's recommended that the precompiler be used in lieu of running the compiler on these legacy environments.
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.1...v2.0.0-alpha.2)
## v2.0.0-alpha.1 - February 10th, 2014
- [#182](https://github.com/wycats/handlebars.js/pull/182) - Allow passing hash parameters to partials ([@kpdecker](https://github.com/kpdecker))
- [#392](https://github.com/wycats/handlebars.js/pull/392) - Access to root context in partials and helpers ([@kpdecker](https://github.com/kpdecker))
- [#472](https://github.com/wycats/handlebars.js/issues/472) - Helpers cannot have decimal parameters ([@kayleg](https://github.com/kayleg))
@@ -625,6 +897,7 @@ Compatibility notes:
- [#704](https://github.com/wycats/handlebars.js/pull/704) - JavaScript Compiler Cleanup ([@blakeembrey](https://github.com/blakeembrey))
Compatibility notes:
- `helperMissing` helper no longer has the indexed name argument. Helper name is now available via `options.name`.
- Precompiler output has changed, which breaks compatibility with prior versions of the runtime and precompiled output.
- `JavaScriptCompiler.compilerInfo` now returns generic objects rather than javascript source.
@@ -637,6 +910,7 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v1.3.0...v2.0.0-alpha.1)
## v1.3.0 - January 1st, 2014
- [#690](https://github.com/wycats/handlebars.js/pull/690) - Added support for subexpressions ([@machty](https://github.com/machty))
- [#696](https://github.com/wycats/handlebars.js/pull/696) - Fix for reserved keyword "default" ([@nateirwin](https://github.com/nateirwin))
- [#692](https://github.com/wycats/handlebars.js/pull/692) - add line numbers to nodes when parsing ([@fivetanley](https://github.com/fivetanley))
@@ -651,12 +925,14 @@ Compatibility notes:
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.1...v1.3.0)
## v1.2.1 - December 26th, 2013
- [#684](https://github.com/wycats/handlebars.js/pull/684) - Allow any number of trailing characters for valid JavaScript variable ([@blakeembrey](https://github.com/blakeembrey))
- [#686](https://github.com/wycats/handlebars.js/pull/686) - Falsy AMD module names in version 1.2.0 ([@kpdecker](https://github.com/kpdecker))
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.0...v1.2.1)
## v1.2.0 - December 23rd, 2013
- [#675](https://github.com/wycats/handlebars.js/issues/675) - Cannot compile empty template for partial ([@erwinw](https://github.com/erwinw))
- [#677](https://github.com/wycats/handlebars.js/issues/677) - Triple brace statements fail under IE ([@hamzaCM](https://github.com/hamzaCM))
- [#655](https://github.com/wycats/handlebars.js/issues/655) - Loading Handlebars using bower ([@niki4810](https://github.com/niki4810))
@@ -676,6 +952,7 @@ Compatibility notes:
- Fix handling of boolean escape in MustacheNode - b4968bb
Compatibility notes:
- `@index` and `@first` are now supported for `each` iteration on objects
- `Handlebars.VM.checkRevision` and `Handlebars.JavaScriptCompiler.prototype.compilerInfo` now available to modify the version checking behavior.
- Browserify users may link to the runtime library via `require('handlebars/runtime')`
@@ -717,16 +994,15 @@ Compatibility notes:
- [#571](https://github.com/wycats/handlebars.js/issues/571) - uglify upgrade breaks compatibility with older versions of node
- [#587](https://github.com/wycats/handlebars.js/issues/587) - Partial inside partial breaks?
Compatibility notes:
- The project now includes separate artifacts for AMD, CommonJS, and global objects.
- The project now includes separate artifacts for AMD, CommonJS, and global objects.
- AMD: Users may load the bundled `handlebars.amd.js` or `handlebars.runtime.amd.js` files or load individual modules directly. AMD users should also note that the handlebars object is exposed via the `default` field on the imported object. This [gist](https://gist.github.com/wycats/7417be0dc361a69d5916) provides some discussion of possible compatibility shims.
- CommonJS/Node: Node loading occurs as normal via `require`
- Globals: The `handlebars.js` and `handlebars.runtime.js` files should behave in the same manner as the v1.0.12 / 1.0.0 release.
- Build artifacts have been removed from the repository. [npm][npm], [components/handlebars.js][components], [cdnjs][cdnjs], or the [builds page][builds-page] should now be used as the source of built artifacts.
- Build artifacts have been removed from the repository. [npm][npm], [components/handlebars.js][components], [cdnjs][cdnjs], or the [builds page][builds-page] should now be used as the source of built artifacts.
- Context-stored helpers are now always passed the `options` hash. Previously no-argument helpers did not have this argument.
[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.12...v1.1.0)
## v1.0.12 / 1.0.0 - May 31 2013
@@ -744,6 +1020,7 @@ Compatibility notes:
- Docs updates
Compatibility notes:
- The parser is now stricter on `{{{`, requiring that the end token be `}}}`. Templates that do not
follow this convention should add the additional brace value.
- Code that relies on global the namespace being muted when custom helpers or partials are passed will need to explicitly pass an `undefined` value for any helpers that should not be available.
@@ -810,13 +1087,13 @@ changed.
Instead of:
```js
template(context, helpers, partials, [data])
template(context, helpers, partials, [data]);
```
Use:
```js
template(context, {helpers: helpers, partials: partials, data: data})
template(context, { helpers: helpers, partials: partials, data: data });
```
[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html
+11 -5
View File
@@ -1,7 +1,8 @@
{
"extends": [
"../.eslintrc.js",
"plugin:es5/no-es2015"
"plugin:es5/no-es2015",
"prettier"
],
"plugins": [
"es5"
@@ -13,6 +14,7 @@
"shouldCompileTo": true,
"shouldCompileToWithPartials": true,
"shouldThrow": true,
"expectTemplate": true,
"compileWithPartials": true,
"console": true,
"require": true,
@@ -26,16 +28,20 @@
"start": true,
"stop": true,
"ok": true,
"sinon": true,
"strictEqual": true,
"define": true
"define": true,
"expect": true,
"chai": true
},
"env": {
"mocha": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,
"no-path-concat": "off",
"no-var": 0
"no-var": "off",
"dot-notation": "off"
}
}
}
+7 -4
View File
@@ -18,13 +18,16 @@
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/require.js"></script>
<script src="/spec/vendor/json2.js"></script>
<script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
@@ -56,7 +59,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+7 -4
View File
@@ -19,13 +19,16 @@
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/require.js"></script>
<script src="/spec/vendor/json2.js"></script>
<script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
@@ -76,7 +79,7 @@
}
var runner = mocha.run();
//Reporting for saucelabs
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
+6
View File
@@ -0,0 +1,6 @@
{{#someHelper true}}
<div>Some known helper</div>
{{#anotherHelper true}}
<div>Another known helper</div>
{{/anotherHelper}}
{{/someHelper}}
+1
View File
@@ -0,0 +1 @@
<div>This is a test</div>
@@ -0,0 +1 @@
<div>Test Partial</div>
+158 -78
View File
@@ -7,44 +7,102 @@ describe('ast', function() {
describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}, Handlebars.Exception, "foo doesn't match bar - 2:5");
shouldThrow(
function() {
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
},
Handlebars.Exception,
"foo doesn't match bar - 2:5"
);
});
});
describe('helpers', function() {
describe('#helperExpression', function() {
it('should handle mustache statements', function() {
equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: undefined}), false);
equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [1], hash: undefined}), true);
equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: {}}), true);
equals(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [],
hash: undefined
}),
false
);
equals(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [1],
hash: undefined
}),
true
);
equals(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [],
hash: {}
}),
true
);
});
it('should handle block statements', function() {
equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: undefined}), false);
equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [1], hash: undefined}), true);
equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: {}}), true);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: undefined
}),
false
);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [1],
hash: undefined
}),
true
);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: {}
}),
true
);
});
it('should handle subexpressions', function() {
equals(AST.helpers.helperExpression({type: 'SubExpression'}), true);
equals(AST.helpers.helperExpression({ type: 'SubExpression' }), true);
});
it('should work with non-helper nodes', function() {
equals(AST.helpers.helperExpression({type: 'Program'}), false);
equals(AST.helpers.helperExpression({ type: 'Program' }), false);
equals(AST.helpers.helperExpression({type: 'PartialStatement'}), false);
equals(AST.helpers.helperExpression({type: 'ContentStatement'}), false);
equals(AST.helpers.helperExpression({type: 'CommentStatement'}), false);
equals(
AST.helpers.helperExpression({ type: 'PartialStatement' }),
false
);
equals(
AST.helpers.helperExpression({ type: 'ContentStatement' }),
false
);
equals(
AST.helpers.helperExpression({ type: 'CommentStatement' }),
false
);
equals(AST.helpers.helperExpression({type: 'PathExpression'}), false);
equals(AST.helpers.helperExpression({ type: 'PathExpression' }), false);
equals(AST.helpers.helperExpression({type: 'StringLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'NumberLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'BooleanLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'UndefinedLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'NullLiteral'}), false);
equals(AST.helpers.helperExpression({ type: 'StringLiteral' }), false);
equals(AST.helpers.helperExpression({ type: 'NumberLiteral' }), false);
equals(AST.helpers.helperExpression({ type: 'BooleanLiteral' }), false);
equals(
AST.helpers.helperExpression({ type: 'UndefinedLiteral' }),
false
);
equals(AST.helpers.helperExpression({ type: 'NullLiteral' }), false);
equals(AST.helpers.helperExpression({type: 'Hash'}), false);
equals(AST.helpers.helperExpression({type: 'HashPair'}), false);
equals(AST.helpers.helperExpression({ type: 'Hash' }), false);
equals(AST.helpers.helperExpression({ type: 'HashPair' }), false);
});
});
});
@@ -61,17 +119,18 @@ describe('ast', function() {
/* eslint-disable no-multi-spaces */
ast = Handlebars.parse(
'line 1 {{line1Token}}\n' // 1
+ ' line 2 {{line2token}}\n' // 2
+ ' line 3 {{#blockHelperOnLine3}}\n' // 3
+ 'line 4{{line4token}}\n' // 4
+ 'line5{{else}}\n' // 5
+ '{{line6Token}}\n' // 6
+ '{{/blockHelperOnLine3}}\n' // 7
+ '{{#open}}\n' // 8
+ '{{else inverse}}\n' // 9
+ '{{else}}\n' // 10
+ '{{/open}}'); // 11
'line 1 {{line1Token}}\n' + // 1
' line 2 {{line2token}}\n' + // 2
' line 3 {{#blockHelperOnLine3}}\n' + // 3
'line 4{{line4token}}\n' + // 4
'line5{{else}}\n' + // 5
'{{line6Token}}\n' + // 6
'{{/blockHelperOnLine3}}\n' + // 7
'{{#open}}\n' + // 8
'{{else inverse}}\n' + // 9
'{{else}}\n' + // 10
'{{/open}}'
); // 11
/* eslint-enable no-multi-spaces */
body = ast.body;
@@ -92,35 +151,35 @@ describe('ast', function() {
it('gets MustacheStatement line numbers correct across newlines', function() {
var secondMustacheStatement = body[3];
testColumns(secondMustacheStatement, 2, 2, 8, 22);
});
});
it('gets the block helper information correct', function() {
var blockHelperNode = body[5];
testColumns(blockHelperNode, 3, 7, 8, 23);
});
it('gets the block helper information correct', function() {
var blockHelperNode = body[5];
testColumns(blockHelperNode, 3, 7, 8, 23);
});
it('correctly records the line numbers the program of a block helper', function() {
var blockHelperNode = body[5],
program = blockHelperNode.program;
it('correctly records the line numbers the program of a block helper', function() {
var blockHelperNode = body[5],
program = blockHelperNode.program;
testColumns(program, 3, 5, 31, 5);
});
testColumns(program, 3, 5, 31, 5);
});
it('correctly records the line numbers of an inverse of a block helper', function() {
var blockHelperNode = body[5],
inverse = blockHelperNode.inverse;
it('correctly records the line numbers of an inverse of a block helper', function() {
var blockHelperNode = body[5],
inverse = blockHelperNode.inverse;
testColumns(inverse, 5, 7, 13, 0);
});
testColumns(inverse, 5, 7, 13, 0);
});
it('correctly records the line number of chained inverses', function() {
var chainInverseNode = body[7];
it('correctly records the line number of chained inverses', function() {
var chainInverseNode = body[7];
testColumns(chainInverseNode.program, 8, 9, 9, 0);
testColumns(chainInverseNode.inverse, 9, 10, 16, 0);
testColumns(chainInverseNode.inverse.body[0].program, 9, 10, 16, 0);
testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
});
testColumns(chainInverseNode.program, 8, 9, 9, 0);
testColumns(chainInverseNode.inverse, 9, 10, 16, 0);
testColumns(chainInverseNode.inverse.body[0].program, 9, 10, 16, 0);
testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
});
});
describe('whitespace control', function() {
@@ -149,7 +208,9 @@ describe('ast', function() {
});
it('block statements', function() {
var ast = Handlebars.parseWithoutProcessing(' {{# comment~}} \nfoo\n {{~/comment}}');
var ast = Handlebars.parseWithoutProcessing(
' {{# comment~}} \nfoo\n {{~/comment}}'
);
equals(ast.body[0].value, ' ');
equals(ast.body[1].program.body[0].value, ' \nfoo\n ');
@@ -167,7 +228,9 @@ describe('ast', function() {
});
describe('blocks - parseWithoutProcessing', function() {
it('block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
var ast = Handlebars.parseWithoutProcessing(
' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].value, ' ');
@@ -178,13 +241,17 @@ describe('ast', function() {
equals(ast.body[2].value, ' ');
});
it('initial block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing('{{# comment}} \nfoo\n {{/comment}}'),
var ast = Handlebars.parseWithoutProcessing(
'{{# comment}} \nfoo\n {{/comment}}'
),
block = ast.body[0];
equals(block.program.body[0].value, ' \nfoo\n ');
});
it('mustaches with children', function() {
var ast = Handlebars.parseWithoutProcessing('{{# comment}} \n{{foo}}\n {{/comment}}'),
var ast = Handlebars.parseWithoutProcessing(
'{{# comment}} \n{{foo}}\n {{/comment}}'
),
block = ast.body[0];
equals(block.program.body[0].value, ' \n');
@@ -192,7 +259,9 @@ describe('ast', function() {
equals(block.program.body[2].value, '\n ');
});
it('nested block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'),
var ast = Handlebars.parseWithoutProcessing(
'{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'
),
body = ast.body[0].program.body,
block = body[1];
@@ -202,8 +271,10 @@ describe('ast', function() {
equals(block.inverse.body[0].value, ' \n bar \n ');
});
it('column 0 block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.body[1];
var ast = Handlebars.parseWithoutProcessing(
'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].omit, undefined);
@@ -215,8 +286,10 @@ describe('ast', function() {
});
describe('blocks', function() {
it('marks block mustaches as standalone', function() {
var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.body[1];
var ast = Handlebars.parse(
' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].value, '');
@@ -227,22 +300,24 @@ describe('ast', function() {
});
it('marks initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
block = ast.body[0];
block = ast.body[0];
equals(block.program.body[0].value, 'foo\n');
});
it('marks mustaches with children as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
block = ast.body[0];
block = ast.body[0];
equals(block.program.body[0].value, '');
equals(block.program.body[1].path.original, 'foo');
equals(block.program.body[2].value, '\n');
});
it('marks nested block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'),
body = ast.body[0].program.body,
block = body[1];
var ast = Handlebars.parse(
'{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'
),
body = ast.body[0].program.body,
block = body[1];
equals(body[0].value, '');
@@ -252,9 +327,11 @@ describe('ast', function() {
equals(body[0].value, '');
});
it('does not mark nested block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'),
body = ast.body[0].program.body,
block = body[1];
var ast = Handlebars.parse(
'{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'
),
body = ast.body[0].program.body,
block = body[1];
equals(body[0].omit, undefined);
@@ -264,9 +341,11 @@ describe('ast', function() {
equals(body[0].omit, undefined);
});
it('does not mark nested initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'),
body = ast.body[0].program.body,
block = body[0];
var ast = Handlebars.parse(
'{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'
),
body = ast.body[0].program.body,
block = body[0];
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
@@ -275,8 +354,10 @@ describe('ast', function() {
});
it('marks column 0 block mustaches as standalone', function() {
var ast = Handlebars.parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.body[1];
var ast = Handlebars.parse(
'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].omit, undefined);
@@ -348,4 +429,3 @@ describe('ast', function() {
});
});
});
+467 -176
View File
@@ -6,300 +6,591 @@ beforeEach(function() {
describe('basic context', function() {
it('most basic', function() {
shouldCompileTo('{{foo}}', { foo: 'foo' }, 'foo');
expectTemplate('{{foo}}')
.withInput({ foo: 'foo' })
.toCompileTo('foo');
});
it('escaping', function() {
shouldCompileTo('\\{{foo}}', { foo: 'food' }, '{{foo}}');
shouldCompileTo('content \\{{foo}}', { foo: 'food' }, 'content {{foo}}');
shouldCompileTo('\\\\{{foo}}', { foo: 'food' }, '\\food');
shouldCompileTo('content \\\\{{foo}}', { foo: 'food' }, 'content \\food');
shouldCompileTo('\\\\ {{foo}}', { foo: 'food' }, '\\\\ food');
expectTemplate('\\{{foo}}')
.withInput({ foo: 'food' })
.toCompileTo('{{foo}}');
expectTemplate('content \\{{foo}}')
.withInput({ foo: 'food' })
.toCompileTo('content {{foo}}');
expectTemplate('\\\\{{foo}}')
.withInput({ foo: 'food' })
.toCompileTo('\\food');
expectTemplate('content \\\\{{foo}}')
.withInput({ foo: 'food' })
.toCompileTo('content \\food');
expectTemplate('\\\\ {{foo}}')
.withInput({ foo: 'food' })
.toCompileTo('\\\\ food');
});
it('compiling with a basic context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world}}!', {cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
'It works if all the required keys are provided');
expectTemplate('Goodbye\n{{cruel}}\n{{world}}!')
.withInput({
cruel: 'cruel',
world: 'world'
})
.withMessage('It works if all the required keys are provided')
.toCompileTo('Goodbye\ncruel\nworld!');
});
it('compiling with a string context', function() {
shouldCompileTo('{{.}}{{length}}', 'bye', 'bye3');
expectTemplate('{{.}}{{length}}')
.withInput('bye')
.toCompileTo('bye3');
});
it('compiling with an undefined context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!');
expectTemplate('Goodbye\n{{cruel}}\n{{world.bar}}!')
.withInput(undefined)
.toCompileTo('Goodbye\n\n!');
shouldCompileTo('{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}', undefined, 'Goodbye');
expectTemplate('{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}')
.withInput(undefined)
.toCompileTo('Goodbye');
});
it('comments', function() {
shouldCompileTo('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!',
{cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
'comments are ignored');
expectTemplate('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!')
.withInput({
cruel: 'cruel',
world: 'world'
})
.withMessage('comments are ignored')
.toCompileTo('Goodbye\ncruel\nworld!');
shouldCompileTo(' {{~! comment ~}} blah', {}, 'blah');
shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah');
shouldCompileTo(' {{! comment ~}} blah', {}, ' blah');
shouldCompileTo(' {{!-- long-comment --~}} blah', {}, ' blah');
shouldCompileTo(' {{~! comment}} blah', {}, ' blah');
shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah');
expectTemplate(' {{~! comment ~}} blah').toCompileTo('blah');
expectTemplate(' {{~!-- long-comment --~}} blah').toCompileTo(
'blah'
);
expectTemplate(' {{! comment ~}} blah').toCompileTo(' blah');
expectTemplate(' {{!-- long-comment --~}} blah').toCompileTo(
' blah'
);
expectTemplate(' {{~! comment}} blah').toCompileTo(' blah');
expectTemplate(' {{~!-- long-comment --}} blah').toCompileTo(
' blah'
);
});
it('boolean', function() {
var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
'booleans show the contents when true');
expectTemplate(string)
.withInput({
goodbye: true,
world: 'world'
})
.withMessage('booleans show the contents when true')
.toCompileTo('GOODBYE cruel world!');
shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
'booleans do not show the contents when false');
expectTemplate(string)
.withInput({
goodbye: false,
world: 'world'
})
.withMessage('booleans do not show the contents when false')
.toCompileTo('cruel world!');
});
it('zeros', function() {
shouldCompileTo('num1: {{num1}}, num2: {{num2}}', {num1: 42, num2: 0},
'num1: 42, num2: 0');
shouldCompileTo('num: {{.}}', 0, 'num: 0');
shouldCompileTo('num: {{num1/num2}}', {num1: {num2: 0}}, 'num: 0');
expectTemplate('num1: {{num1}}, num2: {{num2}}')
.withInput({
num1: 42,
num2: 0
})
.toCompileTo('num1: 42, num2: 0');
expectTemplate('num: {{.}}')
.withInput(0)
.toCompileTo('num: 0');
expectTemplate('num: {{num1/num2}}')
.withInput({ num1: { num2: 0 } })
.toCompileTo('num: 0');
});
it('false', function() {
/* eslint-disable no-new-wrappers */
shouldCompileTo('val1: {{val1}}, val2: {{val2}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{.}}', false, 'val: false');
shouldCompileTo('val: {{val1/val2}}', {val1: {val2: false}}, 'val: false');
expectTemplate('val1: {{val1}}, val2: {{val2}}')
.withInput({
val1: false,
val2: new Boolean(false)
})
.toCompileTo('val1: false, val2: false');
shouldCompileTo('val1: {{{val1}}}, val2: {{{val2}}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{{val1/val2}}}', {val1: {val2: false}}, 'val: false');
expectTemplate('val: {{.}}')
.withInput(false)
.toCompileTo('val: false');
expectTemplate('val: {{val1/val2}}')
.withInput({ val1: { val2: false } })
.toCompileTo('val: false');
expectTemplate('val1: {{{val1}}}, val2: {{{val2}}}')
.withInput({
val1: false,
val2: new Boolean(false)
})
.toCompileTo('val1: false, val2: false');
expectTemplate('val: {{{val1/val2}}}')
.withInput({ val1: { val2: false } })
.toCompileTo('val: false');
/* eslint-enable */
});
it('should handle undefined and null', function() {
shouldCompileTo('{{awesome undefined null}}',
{
awesome: function(_undefined, _null, options) {
return (_undefined === undefined) + ' ' + (_null === null) + ' ' + (typeof options);
}
},
'true true object');
shouldCompileTo('{{undefined}}',
{
'undefined': function() {
return 'undefined!';
}
},
'undefined!');
shouldCompileTo('{{null}}',
{
'null': function() {
return 'null!';
}
},
'null!');
expectTemplate('{{awesome undefined null}}')
.withInput({
awesome: function(_undefined, _null, options) {
return (
(_undefined === undefined) +
' ' +
(_null === null) +
' ' +
typeof options
);
}
})
.toCompileTo('true true object');
expectTemplate('{{undefined}}')
.withInput({
undefined: function() {
return 'undefined!';
}
})
.toCompileTo('undefined!');
expectTemplate('{{null}}')
.withInput({
null: function() {
return 'null!';
}
})
.toCompileTo('null!');
});
it('newlines', function() {
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
expectTemplate("Alan's\nTest").toCompileTo("Alan's\nTest");
expectTemplate("Alan's\rTest").toCompileTo("Alan's\rTest");
});
it('escaping text', function() {
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
shouldCompileTo('Awesome\\', {}, 'Awesome\\', "text is escaped so that the closing quote can't be ignored");
shouldCompileTo('Awesome\\\\ foo', {}, 'Awesome\\\\ foo', "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo('Awesome {{foo}}', {foo: '\\'}, 'Awesome \\', "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(" ' ' ", {}, " ' ' ", 'double quotes never produce invalid javascript');
expectTemplate("Awesome's")
.withMessage(
"text is escaped so that it doesn't get caught on single quotes"
)
.toCompileTo("Awesome's");
expectTemplate('Awesome\\')
.withMessage("text is escaped so that the closing quote can't be ignored")
.toCompileTo('Awesome\\');
expectTemplate('Awesome\\\\ foo')
.withMessage("text is escaped so that it doesn't mess up backslashes")
.toCompileTo('Awesome\\\\ foo');
expectTemplate('Awesome {{foo}}')
.withInput({ foo: '\\' })
.withMessage("text is escaped so that it doesn't mess up backslashes")
.toCompileTo('Awesome \\');
expectTemplate(" ' ' ")
.withMessage('double quotes never produce invalid javascript')
.toCompileTo(" ' ' ");
});
it('escaping expressions', function() {
shouldCompileTo('{{{awesome}}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with 3 handlebars aren't escaped");
expectTemplate('{{{awesome}}}')
.withInput({ awesome: "&'\\<>" })
.withMessage("expressions with 3 handlebars aren't escaped")
.toCompileTo("&'\\<>");
shouldCompileTo('{{&awesome}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with {{& handlebars aren't escaped");
expectTemplate('{{&awesome}}')
.withInput({ awesome: "&'\\<>" })
.withMessage("expressions with {{& handlebars aren't escaped")
.toCompileTo("&'\\<>");
shouldCompileTo('{{awesome}}', {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
'by default expressions should be escaped');
expectTemplate('{{awesome}}')
.withInput({ awesome: '&"\'`\\<>' })
.withMessage('by default expressions should be escaped')
.toCompileTo('&amp;&quot;&#x27;&#x60;\\&lt;&gt;');
shouldCompileTo('{{awesome}}', {awesome: 'Escaped, <b> looks like: &lt;b&gt;'}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
'escaping should properly handle amperstands');
expectTemplate('{{awesome}}')
.withInput({ awesome: 'Escaped, <b> looks like: &lt;b&gt;' })
.withMessage('escaping should properly handle amperstands')
.toCompileTo('Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;');
});
it("functions returning safestrings shouldn't be escaped", function() {
var hash = {awesome: function() { return new Handlebars.SafeString('&\'\\<>'); }};
shouldCompileTo('{{awesome}}', hash, '&\'\\<>',
"functions returning safestrings aren't escaped");
expectTemplate('{{awesome}}')
.withInput({
awesome: function() {
return new Handlebars.SafeString("&'\\<>");
}
})
.withMessage("functions returning safestrings aren't escaped")
.toCompileTo("&'\\<>");
});
it('functions', function() {
shouldCompileTo('{{awesome}}', {awesome: function() { return 'Awesome'; }}, 'Awesome',
'functions are called and render their output');
shouldCompileTo('{{awesome}}', {awesome: function() { return this.more; }, more: 'More awesome'}, 'More awesome',
'functions are bound to the context');
expectTemplate('{{awesome}}')
.withInput({
awesome: function() {
return 'Awesome';
}
})
.withMessage('functions are called and render their output')
.toCompileTo('Awesome');
expectTemplate('{{awesome}}')
.withInput({
awesome: function() {
return this.more;
},
more: 'More awesome'
})
.withMessage('functions are bound to the context')
.toCompileTo('More awesome');
});
it('functions with context argument', function() {
shouldCompileTo('{{awesome frank}}',
{awesome: function(context) { return context; },
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
expectTemplate('{{awesome frank}}')
.withInput({
awesome: function(context) {
return context;
},
frank: 'Frank'
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
});
it('pathed functions with context argument', function() {
shouldCompileTo('{{bar.awesome frank}}',
{bar: {awesome: function(context) { return context; }},
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
expectTemplate('{{bar.awesome frank}}')
.withInput({
bar: {
awesome: function(context) {
return context;
}
},
frank: 'Frank'
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
});
it('depthed functions with context argument', function() {
shouldCompileTo('{{#with frank}}{{../awesome .}}{{/with}}',
{awesome: function(context) { return context; },
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
expectTemplate('{{#with frank}}{{../awesome .}}{{/with}}')
.withInput({
awesome: function(context) {
return context;
},
frank: 'Frank'
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
});
it('block functions with context argument', function() {
shouldCompileTo('{{#awesome 1}}inner {{.}}{{/awesome}}',
{awesome: function(context, options) { return options.fn(context); }},
'inner 1', 'block functions are called with context and options');
expectTemplate('{{#awesome 1}}inner {{.}}{{/awesome}}')
.withInput({
awesome: function(context, options) {
return options.fn(context);
}
})
.withMessage('block functions are called with context and options')
.toCompileTo('inner 1');
});
it('depthed block functions with context argument', function() {
shouldCompileTo('{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}',
{value: true, awesome: function(context, options) { return options.fn(context); }},
'inner 1', 'block functions are called with context and options');
expectTemplate(
'{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}'
)
.withInput({
value: true,
awesome: function(context, options) {
return options.fn(context);
}
})
.withMessage('block functions are called with context and options')
.toCompileTo('inner 1');
});
it('block functions without context argument', function() {
shouldCompileTo('{{#awesome}}inner{{/awesome}}',
{awesome: function(options) { return options.fn(this); }},
'inner', 'block functions are called with options');
});
it('pathed block functions without context argument', function() {
shouldCompileTo('{{#foo.awesome}}inner{{/foo.awesome}}',
{foo: {awesome: function() { return this; }}},
'inner', 'block functions are called with options');
});
it('depthed block functions without context argument', function() {
shouldCompileTo('{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}',
{value: true, awesome: function() { return this; }},
'inner', 'block functions are called with options');
expectTemplate('{{#awesome}}inner{{/awesome}}')
.withInput({
awesome: function(options) {
return options.fn(this);
}
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
});
it('pathed block functions without context argument', function() {
expectTemplate('{{#foo.awesome}}inner{{/foo.awesome}}')
.withInput({
foo: {
awesome: function() {
return this;
}
}
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
});
it('depthed block functions without context argument', function() {
expectTemplate(
'{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}'
)
.withInput({
value: true,
awesome: function() {
return this;
}
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
});
it('paths with hyphens', function() {
shouldCompileTo('{{foo-bar}}', {'foo-bar': 'baz'}, 'baz', 'Paths can contain hyphens (-)');
shouldCompileTo('{{foo.foo-bar}}', {foo: {'foo-bar': 'baz'}}, 'baz', 'Paths can contain hyphens (-)');
shouldCompileTo('{{foo/foo-bar}}', {foo: {'foo-bar': 'baz'}}, 'baz', 'Paths can contain hyphens (-)');
expectTemplate('{{foo-bar}}')
.withInput({ 'foo-bar': 'baz' })
.withMessage('Paths can contain hyphens (-)')
.toCompileTo('baz');
expectTemplate('{{foo.foo-bar}}')
.withInput({ foo: { 'foo-bar': 'baz' } })
.withMessage('Paths can contain hyphens (-)')
.toCompileTo('baz');
expectTemplate('{{foo/foo-bar}}')
.withInput({ foo: { 'foo-bar': 'baz' } })
.withMessage('Paths can contain hyphens (-)')
.toCompileTo('baz');
});
it('nested paths', function() {
shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Nested paths access nested objects');
expectTemplate('Goodbye {{alan/expression}} world!')
.withInput({ alan: { expression: 'beautiful' } })
.withMessage('Nested paths access nested objects')
.toCompileTo('Goodbye beautiful world!');
});
it('nested paths with empty string value', function() {
shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: ''}},
'Goodbye world!', 'Nested paths access nested objects with empty string');
expectTemplate('Goodbye {{alan/expression}} world!')
.withInput({ alan: { expression: '' } })
.withMessage('Nested paths access nested objects with empty string')
.toCompileTo('Goodbye world!');
});
it('literal paths', function() {
shouldCompileTo('Goodbye {{[@alan]/expression}} world!', {'@alan': {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Literal paths can be used');
shouldCompileTo('Goodbye {{[foo bar]/expression}} world!', {'foo bar': {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Literal paths can be used');
expectTemplate('Goodbye {{[@alan]/expression}} world!')
.withInput({ '@alan': { expression: 'beautiful' } })
.withMessage('Literal paths can be used')
.toCompileTo('Goodbye beautiful world!');
expectTemplate('Goodbye {{[foo bar]/expression}} world!')
.withInput({ 'foo bar': { expression: 'beautiful' } })
.withMessage('Literal paths can be used')
.toCompileTo('Goodbye beautiful world!');
});
it('literal references', function() {
shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo('Goodbye {{"foo bar"}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo("Goodbye {{'foo bar'}} world!", {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo('Goodbye {{"foo[bar"}} world!', {'foo[bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo('Goodbye {{"foo\'bar"}} world!', {"foo'bar": 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo("Goodbye {{'foo\"bar'}} world!", {'foo"bar': 'beautiful'}, 'Goodbye beautiful world!');
expectTemplate('Goodbye {{[foo bar]}} world!')
.withInput({ 'foo bar': 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
expectTemplate('Goodbye {{"foo bar"}} world!')
.withInput({ 'foo bar': 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
expectTemplate("Goodbye {{'foo bar'}} world!")
.withInput({ 'foo bar': 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
expectTemplate('Goodbye {{"foo[bar"}} world!')
.withInput({ 'foo[bar': 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
expectTemplate('Goodbye {{"foo\'bar"}} world!')
.withInput({ "foo'bar": 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
expectTemplate("Goodbye {{'foo\"bar'}} world!")
.withInput({ 'foo"bar': 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
});
it("that current context path ({{.}}) doesn't hit helpers", function() {
shouldCompileTo('test: {{.}}', [null, {helper: 'awesome'}], 'test: ');
expectTemplate('test: {{.}}')
.withInput(null)
.withHelpers({ helper: 'awesome' })
.toCompileTo('test: ');
});
it('complex but empty paths', function() {
shouldCompileTo('{{person/name}}', {person: {name: null}}, '');
shouldCompileTo('{{person/name}}', {person: {}}, '');
expectTemplate('{{person/name}}')
.withInput({ person: { name: null } })
.toCompileTo('');
expectTemplate('{{person/name}}')
.withInput({ person: {} })
.toCompileTo('');
});
it('this keyword in paths', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}';
var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, hash, 'goodbyeGoodbyeGOODBYE',
'This keyword in paths evaluates to current context');
expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}')
.withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] })
.withMessage('This keyword in paths evaluates to current context')
.toCompileTo('goodbyeGoodbyeGOODBYE');
string = '{{#hellos}}{{this/text}}{{/hellos}}';
hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, hash, 'helloHelloHELLO', 'This keyword evaluates in more complex paths');
expectTemplate('{{#hellos}}{{this/text}}{{/hellos}}')
.withInput({
hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }]
})
.withMessage('This keyword evaluates in more complex paths')
.toCompileTo('helloHelloHELLO');
});
it('this keyword nested inside path', function() {
shouldThrow(function() {
CompilerContext.compile('{{#hellos}}{{text/this/foo}}{{/hellos}}');
}, Error, 'Invalid path: text/this - 1:13');
expectTemplate('{{#hellos}}{{text/this/foo}}{{/hellos}}').toThrow(
Error,
'Invalid path: text/this - 1:13'
);
shouldCompileTo('{{[this]}}', {'this': 'bar'}, 'bar');
shouldCompileTo('{{text/[this]}}', {text: {'this': 'bar'}}, 'bar');
expectTemplate('{{[this]}}')
.withInput({ this: 'bar' })
.toCompileTo('bar');
expectTemplate('{{text/[this]}}')
.withInput({ text: { this: 'bar' } })
.toCompileTo('bar');
});
it('this keyword in helpers', function() {
var helpers = {foo: function(value) {
var helpers = {
foo: function(value) {
return 'bar ' + value;
}};
var string = '{{#goodbyes}}{{foo this}}{{/goodbyes}}';
var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, [hash, helpers], 'bar goodbyebar Goodbyebar GOODBYE',
'This keyword in paths evaluates to current context');
}
};
string = '{{#hellos}}{{foo this/text}}{{/hellos}}';
hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, [hash, helpers], 'bar hellobar Hellobar HELLO', 'This keyword evaluates in more complex paths');
expectTemplate('{{#goodbyes}}{{foo this}}{{/goodbyes}}')
.withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] })
.withHelpers(helpers)
.withMessage('This keyword in paths evaluates to current context')
.toCompileTo('bar goodbyebar Goodbyebar GOODBYE');
expectTemplate('{{#hellos}}{{foo this/text}}{{/hellos}}')
.withInput({
hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }]
})
.withHelpers(helpers)
.withMessage('This keyword evaluates in more complex paths')
.toCompileTo('bar hellobar Hellobar HELLO');
});
it('this keyword nested inside helpers param', function() {
var string = '{{#hellos}}{{foo text/this/foo}}{{/hellos}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error, 'Invalid path: text/this - 1:17');
expectTemplate('{{#hellos}}{{foo text/this/foo}}{{/hellos}}').toThrow(
Error,
'Invalid path: text/this - 1:17'
);
shouldCompileTo(
'{{foo [this]}}',
{foo: function(value) { return value; }, 'this': 'bar'},
'bar');
shouldCompileTo(
'{{foo text/[this]}}',
{foo: function(value) { return value; }, text: {'this': 'bar'}},
'bar');
expectTemplate('{{foo [this]}}')
.withInput({
foo: function(value) {
return value;
},
this: 'bar'
})
.toCompileTo('bar');
expectTemplate('{{foo text/[this]}}')
.withInput({
foo: function(value) {
return value;
},
text: { this: 'bar' }
})
.toCompileTo('bar');
});
it('pass string literals', function() {
shouldCompileTo('{{"foo"}}', {}, '');
shouldCompileTo('{{"foo"}}', { foo: 'bar' }, 'bar');
shouldCompileTo('{{#"foo"}}{{.}}{{/"foo"}}', { foo: ['bar', 'baz'] }, 'barbaz');
expectTemplate('{{"foo"}}').toCompileTo('');
expectTemplate('{{"foo"}}')
.withInput({ foo: 'bar' })
.toCompileTo('bar');
expectTemplate('{{#"foo"}}{{.}}{{/"foo"}}')
.withInput({
foo: ['bar', 'baz']
})
.toCompileTo('barbaz');
});
it('pass number literals', function() {
shouldCompileTo('{{12}}', {}, '');
shouldCompileTo('{{12}}', { '12': 'bar' }, 'bar');
shouldCompileTo('{{12.34}}', {}, '');
shouldCompileTo('{{12.34}}', { '12.34': 'bar' }, 'bar');
shouldCompileTo('{{12.34 1}}', { '12.34': function(arg) { return 'bar' + arg; } }, 'bar1');
expectTemplate('{{12}}').toCompileTo('');
expectTemplate('{{12}}')
.withInput({ '12': 'bar' })
.toCompileTo('bar');
expectTemplate('{{12.34}}').toCompileTo('');
expectTemplate('{{12.34}}')
.withInput({ '12.34': 'bar' })
.toCompileTo('bar');
expectTemplate('{{12.34 1}}')
.withInput({
'12.34': function(arg) {
return 'bar' + arg;
}
})
.toCompileTo('bar1');
});
it('pass boolean literals', function() {
shouldCompileTo('{{true}}', {}, '');
shouldCompileTo('{{true}}', { '': 'foo' }, '');
shouldCompileTo('{{false}}', { 'false': 'foo' }, 'foo');
expectTemplate('{{true}}').toCompileTo('');
expectTemplate('{{true}}')
.withInput({ '': 'foo' })
.toCompileTo('');
expectTemplate('{{false}}')
.withInput({ false: 'foo' })
.toCompileTo('foo');
});
it('should handle literals in subexpression', function() {
var helpers = {
foo: function(arg) {
expectTemplate('{{foo (false)}}')
.withInput({
false: function() {
return 'bar';
}
})
.withHelper('foo', function(arg) {
return arg;
}
};
shouldCompileTo('{{foo (false)}}', [{ 'false': function() { return 'bar'; } }, helpers], 'bar');
})
.toCompileTo('bar');
});
});
+304 -195
View File
@@ -1,308 +1,405 @@
describe('blocks', function() {
it('array', function() {
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'Arrays ignore the contents when empty');
expectTemplate(string)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('Arrays iterate over the contents when not empty')
.toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
})
.withMessage('Arrays ignore the contents when empty')
.toCompileTo('cruel world!');
});
it('array without data', function() {
var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,, false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
expectTemplate(
'{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withCompileOptions({ compat: false })
.toCompileTo('goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
});
it('array with @index', function() {
var string = '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
expectTemplate(
'{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @index variable is used')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('empty block', function() {
var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'cruel world!',
'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'Arrays ignore the contents when empty');
expectTemplate(string)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('Arrays iterate over the contents when not empty')
.toCompileTo('cruel world!');
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
})
.withMessage('Arrays ignore the contents when empty')
.toCompileTo('cruel world!');
});
it('block with complex lookup', function() {
var string = '{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}';
var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
shouldCompileTo(string, hash, 'goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ',
'Templates can access variables in contexts up the stack with relative path syntax');
expectTemplate('{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}')
.withInput({
name: 'Alan',
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
]
})
.withMessage(
'Templates can access variables in contexts up the stack with relative path syntax'
)
.toCompileTo(
'goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! '
);
});
it('multiple blocks with complex lookup', function() {
var string = '{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}';
var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan');
expectTemplate('{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}')
.withInput({
name: 'Alan',
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
]
})
.toCompileTo('AlanAlanAlanAlanAlanAlan');
});
it('block with complex lookup using nested context', function() {
var string = '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
expectTemplate(
'{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}'
).toThrow(Error);
});
it('block with deep nested complex lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}';
var hash = {omg: 'OMG!', outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }] };
shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!');
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}'
)
.withInput({
omg: 'OMG!',
outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }]
})
.toCompileTo('Goodbye cruel sad OMG!');
});
it('works with cached blocks', function() {
var template = CompilerContext.compile('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}', {data: false});
var result = template({person: [{first: 'Alan', last: 'Johnson'}, {first: 'Alan', last: 'Johnson'}]});
equals(result, 'Alan JohnsonAlan Johnson');
expectTemplate(
'{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}'
)
.withCompileOptions({ data: false })
.withInput({
person: [
{ first: 'Alan', last: 'Johnson' },
{ first: 'Alan', last: 'Johnson' }
]
})
.toCompileTo('Alan JohnsonAlan Johnson');
});
describe('inverted sections', function() {
it('inverted sections with unset value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {};
shouldCompileTo(string, hash, 'Right On!', "Inverted section rendered when value isn't set.");
expectTemplate(
'{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
)
.withMessage("Inverted section rendered when value isn't set.")
.toCompileTo('Right On!');
});
it('inverted section with false value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: false};
shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is false.');
expectTemplate(
'{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
)
.withInput({ goodbyes: false })
.withMessage('Inverted section rendered when value is false.')
.toCompileTo('Right On!');
});
it('inverted section with empty set', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: []};
shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is empty set.');
expectTemplate(
'{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
)
.withInput({ goodbyes: [] })
.withMessage('Inverted section rendered when value is empty set.')
.toCompileTo('Right On!');
});
it('block inverted sections', function() {
shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people'},
'No people');
expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
.withInput({ none: 'No people' })
.toCompileTo('No people');
});
it('chained inverted sections', function() {
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}', {none: 'No people'},
'No people');
shouldCompileTo('{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}', {none: 'No people'},
'No people');
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}', {none: 'No people'},
'No people');
expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}')
.withInput({ none: 'No people' })
.toCompileTo('No people');
expectTemplate(
'{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}'
)
.withInput({ none: 'No people' })
.toCompileTo('No people');
expectTemplate(
'{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}'
)
.withInput({ none: 'No people' })
.toCompileTo('No people');
});
it('chained inverted sections with mismatch', function() {
shouldThrow(function() {
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}', {none: 'No people'},
'No people');
}, Error);
expectTemplate(
'{{#people}}{{name}}{{else if none}}{{none}}{{/if}}'
).toThrow(Error);
});
it('block inverted sections with empty arrays', function() {
shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people', people: []},
'No people');
expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
.withInput({
none: 'No people',
people: []
})
.toCompileTo('No people');
});
});
describe('standalone sections', function() {
it('block standalone else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
shouldCompileTo('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n', {none: 'No people'},
'No people\n');
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
.withInput({ none: 'No people' })
.toCompileTo('No people\n');
expectTemplate('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n')
.withInput({ none: 'No people' })
.toCompileTo('No people\n');
expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
.withInput({ none: 'No people' })
.toCompileTo('No people\n');
});
it('block standalone else sections can be disabled', function() {
shouldCompileTo(
'{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n',
[{none: 'No people'}, {}, {}, {ignoreStandalone: true}],
'\nNo people\n\n');
shouldCompileTo(
'{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n',
[{none: 'No people'}, {}, {}, {ignoreStandalone: true}],
'\nNo people\n\n');
expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
.withInput({ none: 'No people' })
.withCompileOptions({ ignoreStandalone: true })
.toCompileTo('\nNo people\n\n');
expectTemplate('{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n')
.withInput({ none: 'No people' })
.withCompileOptions({ ignoreStandalone: true })
.toCompileTo('\nNo people\n\n');
});
it('block standalone chained else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
expectTemplate(
'{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n'
)
.withInput({ none: 'No people' })
.toCompileTo('No people\n');
expectTemplate(
'{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n'
)
.withInput({ none: 'No people' })
.toCompileTo('No people\n');
});
it('should handle nesting', function() {
shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.');
expectTemplate('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.')
.withInput({
data: [1, 3, 5]
})
.toCompileTo('1\n3\n5\nOK.');
});
});
describe('compat mode', function() {
it('block with deep recursive lookup lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}';
var hash = {omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!');
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}'
)
.withInput({ omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] })
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel OMG!');
});
it('block with deep recursive pathed lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {yes: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!');
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'
)
.withInput({
omg: { yes: 'OMG!' },
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }]
})
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel OMG!');
});
it('block with missed recursive lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {no: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel ');
it('block with missed recursive lookup', function() {
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'
)
.withInput({
omg: { no: 'OMG!' },
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }]
})
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel ');
});
});
describe('decorators', function() {
it('should apply mustache decorators', function() {
var helpers = {
helper: function(options) {
expectTemplate('{{#helper}}{{*decorator}}{{/helper}}')
.withHelper('helper', function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn) {
})
.withDecorator('decorator', function(fn) {
fn.run = 'success';
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{*decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
})
.toCompileTo('success');
});
it('should apply allow undefined return', function() {
var helpers = {
helper: function(options) {
expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}')
.withHelper('helper', function(options) {
return options.fn() + options.fn.run;
}
};
var decorators = {
decorator: function(fn) {
})
.withDecorator('decorator', function(fn) {
fn.run = 'cess';
}
};
shouldCompileTo(
'{{#helper}}{{*decorator}}suc{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
})
.toCompileTo('success');
});
it('should apply block decorators', function() {
var helpers = {
helper: function(options) {
expectTemplate(
'{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}'
)
.withHelper('helper', function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
})
.withDecorator('decorator', function(fn, props, container, options) {
fn.run = options.fn();
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
})
.toCompileTo('success');
});
it('should support nested decorators', function() {
var helpers = {
helper: function(options) {
expectTemplate(
'{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}'
)
.withHelper('helper', function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.fn.nested + options.fn();
return fn;
},
nested: function(fn, props, container, options) {
props.nested = options.fn();
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
})
.withDecorators({
decorator: function(fn, props, container, options) {
fn.run = options.fn.nested + options.fn();
return fn;
},
nested: function(fn, props, container, options) {
props.nested = options.fn();
}
})
.toCompileTo('success');
});
it('should apply multiple decorators', function() {
var helpers = {
helper: function(options) {
expectTemplate(
'{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}'
)
.withHelper('helper', function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
})
.withDecorator('decorator', function(fn, props, container, options) {
fn.run = (fn.run || '') + options.fn();
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
})
.toCompileTo('success');
});
it('should access parent variables', function() {
var helpers = {
helper: function(options) {
expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}')
.withHelper('helper', function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
})
.withDecorator('decorator', function(fn, props, container, options) {
fn.run = options.args;
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{*decorator foo}}{{/helper}}',
{hash: {'foo': 'success'}, helpers: helpers, decorators: decorators},
'success');
})
.withInput({ foo: 'success' })
.toCompileTo('success');
});
it('should work with root program', function() {
var run;
var decorators = {
decorator: function(fn, props, container, options) {
expectTemplate('{{*decorator "success"}}')
.withDecorator('decorator', function(fn, props, container, options) {
equals(options.args[0], 'success');
run = true;
return fn;
}
};
shouldCompileTo(
'{{*decorator "success"}}',
{hash: {'foo': 'success'}, decorators: decorators},
'');
})
.withInput({ foo: 'success' })
.toCompileTo('');
equals(run, true);
});
it('should fail when accessing variables from root', function() {
var run;
var decorators = {
decorator: function(fn, props, container, options) {
expectTemplate('{{*decorator foo}}')
.withDecorator('decorator', function(fn, props, container, options) {
equals(options.args[0], undefined);
run = true;
return fn;
}
};
shouldCompileTo(
'{{*decorator foo}}',
{hash: {'foo': 'fail'}, decorators: decorators},
'');
})
.withInput({ foo: 'fail' })
.toCompileTo('');
equals(run, true);
});
@@ -334,13 +431,25 @@ describe('blocks', function() {
equals(handlebarsEnv.decorators.foo, undefined);
equals(handlebarsEnv.decorators.bar, undefined);
});
it('fails with multiple and args', function() {
shouldThrow(function() {
handlebarsEnv.registerDecorator({
world: function() { return 'world!'; },
testHelper: function() { return 'found it!'; }
}, {});
}, Error, 'Arg not supported with multiple decorators');
shouldThrow(
function() {
handlebarsEnv.registerDecorator(
{
world: function() {
return 'world!';
},
testHelper: function() {
return 'found it!';
}
},
{}
);
},
Error,
'Arg not supported with multiple decorators'
);
});
});
});
+510 -217
View File
@@ -2,65 +2,184 @@ describe('builtin helpers', function() {
describe('#if', function() {
it('if', function() {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
'if with boolean argument shows the contents when true');
shouldCompileTo(string, {goodbye: 'dummy', world: 'world'}, 'GOODBYE cruel world!',
'if with string argument shows the contents');
shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
'if with boolean argument does not show the contents when false');
shouldCompileTo(string, {world: 'world'}, 'cruel world!',
'if with undefined does not show the contents');
shouldCompileTo(string, {goodbye: ['foo'], world: 'world'}, 'GOODBYE cruel world!',
'if with non-empty array shows the contents');
shouldCompileTo(string, {goodbye: [], world: 'world'}, 'cruel world!',
'if with empty array does not show the contents');
shouldCompileTo(string, {goodbye: 0, world: 'world'}, 'cruel world!',
'if with zero does not show the contents');
shouldCompileTo('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!',
{goodbye: 0, world: 'world'}, 'GOODBYE cruel world!',
'if with zero does not show the contents');
expectTemplate(string)
.withInput({
goodbye: true,
world: 'world'
})
.withMessage('if with boolean argument shows the contents when true')
.toCompileTo('GOODBYE cruel world!');
expectTemplate(string)
.withInput({
goodbye: 'dummy',
world: 'world'
})
.withMessage('if with string argument shows the contents')
.toCompileTo('GOODBYE cruel world!');
expectTemplate(string)
.withInput({
goodbye: false,
world: 'world'
})
.withMessage(
'if with boolean argument does not show the contents when false'
)
.toCompileTo('cruel world!');
expectTemplate(string)
.withInput({ world: 'world' })
.withMessage('if with undefined does not show the contents')
.toCompileTo('cruel world!');
expectTemplate(string)
.withInput({
goodbye: ['foo'],
world: 'world'
})
.withMessage('if with non-empty array shows the contents')
.toCompileTo('GOODBYE cruel world!');
expectTemplate(string)
.withInput({
goodbye: [],
world: 'world'
})
.withMessage('if with empty array does not show the contents')
.toCompileTo('cruel world!');
expectTemplate(string)
.withInput({
goodbye: 0,
world: 'world'
})
.withMessage('if with zero does not show the contents')
.toCompileTo('cruel world!');
expectTemplate(
'{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!'
)
.withInput({
goodbye: 0,
world: 'world'
})
.withMessage('if with zero does not show the contents')
.toCompileTo('GOODBYE cruel world!');
});
it('if with function argument', function() {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: function() {return true; }, world: 'world'}, 'GOODBYE cruel world!',
'if with function shows the contents when function returns true');
shouldCompileTo(string, {goodbye: function() {return this.world; }, world: 'world'}, 'GOODBYE cruel world!',
'if with function shows the contents when function returns string');
shouldCompileTo(string, {goodbye: function() {return false; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns false');
shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns undefined');
expectTemplate(string)
.withInput({
goodbye: function() {
return true;
},
world: 'world'
})
.withMessage(
'if with function shows the contents when function returns true'
)
.toCompileTo('GOODBYE cruel world!');
expectTemplate(string)
.withInput({
goodbye: function() {
return this.world;
},
world: 'world'
})
.withMessage(
'if with function shows the contents when function returns string'
)
.toCompileTo('GOODBYE cruel world!');
expectTemplate(string)
.withInput({
goodbye: function() {
return false;
},
world: 'world'
})
.withMessage(
'if with function does not show the contents when returns false'
)
.toCompileTo('cruel world!');
expectTemplate(string)
.withInput({
goodbye: function() {
return this.foo;
},
world: 'world'
})
.withMessage(
'if with function does not show the contents when returns undefined'
)
.toCompileTo('cruel world!');
});
it('should not change the depth list', function() {
var string = '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}';
shouldCompileTo(string, {foo: {goodbye: true}, world: 'world'}, 'GOODBYE cruel world!');
expectTemplate(
'{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}'
)
.withInput({
foo: { goodbye: true },
world: 'world'
})
.toCompileTo('GOODBYE cruel world!');
});
});
describe('#with', function() {
it('with', function() {
var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}')
.withInput({
person: {
first: 'Alan',
last: 'Johnson'
}
})
.toCompileTo('Alan Johnson');
});
it('with with function argument', function() {
var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: function() { return {first: 'Alan', last: 'Johnson'}; }}, 'Alan Johnson');
});
it('with with else', function() {
var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}';
shouldCompileTo(string, {}, 'Person is not present');
});
it('with provides block parameter', function() {
var string = '{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
});
it('works when data is disabled', function() {
var template = CompilerContext.compile('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}', {data: false});
var result = template({person: {first: 'Alan', last: 'Johnson'}});
equals(result, 'Alan Johnson');
it('with with function argument', function() {
expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}')
.withInput({
person: function() {
return {
first: 'Alan',
last: 'Johnson'
};
}
})
.toCompileTo('Alan Johnson');
});
it('with with else', function() {
expectTemplate(
'{{#with person}}Person is present{{else}}Person is not present{{/with}}'
).toCompileTo('Person is not present');
});
it('with provides block parameter', function() {
expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}')
.withInput({
person: {
first: 'Alan',
last: 'Johnson'
}
})
.toCompileTo('Alan Johnson');
});
it('works when data is disabled', function() {
expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}')
.withInput({ person: { first: 'Alan', last: 'Johnson' } })
.withCompileOptions({ data: false })
.toCompileTo('Alan Johnson');
});
});
@@ -73,186 +192,320 @@ describe('builtin helpers', function() {
it('each', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'each with array argument ignores the contents when empty');
expectTemplate(string)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage(
'each with array argument iterates over the contents when not empty'
)
.toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
})
.withMessage('each with array argument ignores the contents when empty')
.toCompileTo('cruel world!');
});
it('each without data', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,,, false], 'goodbye! Goodbye! GOODBYE! cruel world!');
expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!')
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withRuntimeOptions({ data: false })
.withCompileOptions({ data: false })
.toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
hash = {goodbyes: 'cruel', world: 'world'};
shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,, false], 'cruelworld');
expectTemplate('{{#each .}}{{.}}{{/each}}')
.withInput({ goodbyes: 'cruel', world: 'world' })
.withRuntimeOptions({ data: false })
.withCompileOptions({ data: false })
.toCompileTo('cruelworld');
});
it('each without context', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
shouldCompileTo(string, [,,,, ], 'cruel !');
expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!')
.withInput(undefined)
.toCompileTo('cruel !');
});
it('each with an object and @key', function() {
var string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
var string =
'{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
function Clazz() {
this['<b>#1</b>'] = {text: 'goodbye'};
this[2] = {text: 'GOODBYE'};
this['<b>#1</b>'] = { text: 'goodbye' };
this[2] = { text: 'GOODBYE' };
}
Clazz.prototype.foo = 'fail';
var hash = {goodbyes: new Clazz(), world: 'world'};
var hash = { goodbyes: new Clazz(), world: 'world' };
// Object property iteration order is undefined according to ECMA spec,
// so we need to check both possible orders
// @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
var actual = compileWithPartials(string, hash);
var expected1 = '&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!';
var expected2 = '2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!';
var expected1 =
'&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!';
var expected2 =
'2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!';
equals(actual === expected1 || actual === expected2, true, 'each with object argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: {}, world: 'world'}, 'cruel world!');
equals(
actual === expected1 || actual === expected2,
true,
'each with object argument iterates over the contents when not empty'
);
expectTemplate(string)
.withInput({
goodbyes: {},
world: 'world'
})
.toCompileTo('cruel world!');
});
it('each with @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @index variable is used')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('each with nested @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!', 'The @index variable is used');
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @index variable is used')
.toCompileTo(
'0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!'
);
});
it('each with block params', function() {
var string = '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'};
expectTemplate(
'{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!'
)
.withInput({
goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }],
world: 'world'
})
.toCompileTo(
'0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'
);
});
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!');
it('each with block params and strict compilation', function() {
expectTemplate(
'{{#each goodbyes as |value index|}}{{index}}. {{value.text}}!{{/each}}'
)
.withCompileOptions({ strict: true })
.withInput({ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }] })
.toCompileTo('0. goodbye!1. Goodbye!');
});
it('each object with @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, c: {text: 'GOODBYE'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: {
a: { text: 'goodbye' },
b: { text: 'Goodbye' },
c: { text: 'GOODBYE' }
},
world: 'world'
})
.withMessage('The @index variable is used')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('each with @first', function() {
var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'goodbye! cruel world!', 'The @first variable is used');
expectTemplate(
'{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @first variable is used')
.toCompileTo('goodbye! cruel world!');
});
it('each with nested @first', function() {
var string = '{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!', 'The @first variable is used');
expectTemplate(
'{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @first variable is used')
.toCompileTo(
'(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!'
);
});
it('each object with @first', function() {
var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'goodbye! cruel world!', 'The @first variable is used');
expectTemplate(
'{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
world: 'world'
})
.withMessage('The @first variable is used')
.toCompileTo('goodbye! cruel world!');
});
it('each with @last', function() {
var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'GOODBYE! cruel world!', 'The @last variable is used');
expectTemplate(
'{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @last variable is used')
.toCompileTo('GOODBYE! cruel world!');
});
it('each object with @last', function() {
var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'Goodbye! cruel world!', 'The @last variable is used');
expectTemplate(
'{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
world: 'world'
})
.withMessage('The @last variable is used')
.toCompileTo('Goodbye! cruel world!');
});
it('each with nested @last', function() {
var string = '{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!', 'The @last variable is used');
expectTemplate(
'{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
],
world: 'world'
})
.withMessage('The @last variable is used')
.toCompileTo(
'(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!'
);
});
it('each with function argument', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: function() { return [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]; }, world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array function argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'each with array function argument ignores the contents when empty');
expectTemplate(string)
.withInput({
goodbyes: function() {
return [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
];
},
world: 'world'
})
.withMessage(
'each with array function argument iterates over the contents when not empty'
)
.toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
})
.withMessage(
'each with array function argument ignores the contents when empty'
)
.toCompileTo('cruel world!');
});
it('each object when last key is an empty string', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, '': {text: 'GOODBYE'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'Empty string key is not skipped');
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: {
a: { text: 'goodbye' },
b: { text: 'Goodbye' },
'': { text: 'GOODBYE' }
},
world: 'world'
})
.withMessage('Empty string key is not skipped')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('data passed to helpers', function() {
var string = '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}';
var hash = {letters: ['a', 'b', 'c']};
var template = CompilerContext.compile(string);
var result = template(hash, {
data: {
exclaim: '!'
}
});
equal(result, 'a!b!c!', 'should output data');
expectTemplate(
'{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}'
)
.withInput({ letters: ['a', 'b', 'c'] })
.withMessage('should output data')
.withRuntimeOptions({
data: {
exclaim: '!'
}
})
.toCompileTo('a!b!c!');
});
it('each on implicit context', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{#each}}{{text}}! {{/each}}cruel world!');
template({});
}, handlebarsEnv.Exception, 'Must pass iterator to #each');
expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow(
handlebarsEnv.Exception,
'Must pass iterator to #each'
);
});
if (global.Symbol && global.Symbol.iterator) {
@@ -276,13 +529,30 @@ describe('builtin helpers', function() {
return new Iterator(this.arr);
};
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var goodbyes = new Iterable([{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]);
var goodbyesEmpty = new Iterable([]);
var hash = {goodbyes: goodbyes, world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: goodbyesEmpty, world: 'world'}, 'cruel world!',
'each with array argument ignores the contents when empty');
expectTemplate(string)
.withInput({
goodbyes: new Iterable([
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
]),
world: 'world'
})
.withMessage(
'each with array argument iterates over the contents when not empty'
)
.toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
expectTemplate(string)
.withInput({
goodbyes: new Iterable([]),
world: 'world'
})
.withMessage(
'each with array argument ignores the contents when empty'
)
.toCompileTo('cruel world!');
});
}
});
@@ -293,9 +563,7 @@ describe('builtin helpers', function() {
return;
}
var $log,
$info,
$error;
var $log, $info, $error;
beforeEach(function() {
$log = console.log;
$info = console.info;
@@ -308,36 +576,37 @@ describe('builtin helpers', function() {
});
it('should call logger at default level', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var levelArg, logArg;
handlebarsEnv.log = function(level, arg) {
levelArg = level;
logArg = arg;
};
shouldCompileTo(string, hash, '', 'log should not display');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.withMessage('log should not display')
.toCompileTo('');
equals(1, levelArg, 'should call log with 1');
equals('whee', logArg, "should call log with 'whee'");
});
it('should call logger at data level', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
it('should call logger at data level', function() {
var levelArg, logArg;
handlebarsEnv.log = function(level, arg) {
levelArg = level;
logArg = arg;
};
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals('03', levelArg);
equals('whee', logArg);
});
it('should output to info', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.info = function(info) {
@@ -353,12 +622,13 @@ describe('builtin helpers', function() {
console.log = $log;
};
shouldCompileTo(string, hash, '');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(true, called);
});
it('should log at data level', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
@@ -367,13 +637,16 @@ describe('builtin helpers', function() {
console.error = $error;
};
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
});
it('should handle missing logger', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' },
called = false;
var called = false;
console.error = undefined;
console.log = function(log) {
@@ -382,13 +655,15 @@ describe('builtin helpers', function() {
console.log = $log;
};
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
});
it('should handle string log levels', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
@@ -396,17 +671,24 @@ describe('builtin helpers', function() {
called = true;
};
shouldCompileTo(string, [hash,,,, {level: 'error'}], '');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.withRuntimeOptions({ data: { level: 'error' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
called = false;
shouldCompileTo(string, [hash,,,, {level: 'ERROR'}], '');
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.withRuntimeOptions({ data: { level: 'ERROR' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
});
it('should handle hash log levels', function() {
var string = '{{log blah level="error"}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
@@ -414,12 +696,13 @@ describe('builtin helpers', function() {
called = true;
};
shouldCompileTo(string, hash, '');
expectTemplate('{{log blah level="error"}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(true, called);
});
it('should handle hash log levels', function() {
var string = '{{log blah level="debug"}}';
var hash = { blah: 'whee' };
var called = false;
console.info = console.log = console.error = console.debug = function() {
@@ -427,12 +710,13 @@ describe('builtin helpers', function() {
console.info = console.log = console.error = console.debug = $log;
};
shouldCompileTo(string, hash, '');
expectTemplate('{{log blah level="debug"}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(false, called);
});
it('should pass multiple log arguments', function() {
var string = '{{log blah "foo" 1}}';
var hash = { blah: 'whee' };
var called;
console.info = console.log = function(log1, log2, log3) {
@@ -443,31 +727,40 @@ describe('builtin helpers', function() {
console.log = $log;
};
shouldCompileTo(string, hash, '');
expectTemplate('{{log blah "foo" 1}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(true, called);
});
it('should pass zero log arguments', function() {
var called;
console.info = console.log = function() {
expect(arguments.length).to.equal(0);
called = true;
console.log = $log;
};
expectTemplate('{{log}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).to.be.true();
});
/* eslint-enable no-console */
});
describe('#lookup', function() {
it('should lookup arbitrary content', function() {
var string = '{{#each goodbyes}}{{lookup ../data .}}{{/each}}',
hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'foobar');
expectTemplate('{{#each goodbyes}}{{lookup ../data .}}{{/each}}')
.withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] })
.toCompileTo('foobar');
});
it('should not fail on undefined value', function() {
var string = '{{#each goodbyes}}{{lookup ../bar .}}{{/each}}',
hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '');
expectTemplate('{{#each goodbyes}}{{lookup ../bar .}}{{/each}}')
.withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] })
.toCompileTo('');
});
});
});
+112 -35
View File
@@ -13,37 +13,84 @@ describe('compiler', function() {
equal(compile('foo').equals(compile('foo')), true);
equal(compile('{{foo}}').equals(compile('{{foo}}')), true);
equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true);
equal(compile('{{foo.bar baz "foo" true false bat=1}}').equals(compile('{{foo.bar baz "foo" true false bat=1}}')), true);
equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (baz bat=1)}}')), true);
equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')), true);
equal(
compile('{{foo.bar baz "foo" true false bat=1}}').equals(
compile('{{foo.bar baz "foo" true false bat=1}}')
),
true
);
equal(
compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (baz bat=1)}}')
),
true
);
equal(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')),
true
);
});
it('should treat as not equal', function() {
equal(compile('foo').equals(compile('bar')), false);
equal(compile('{{foo}}').equals(compile('{{bar}}')), false);
equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false);
equal(compile('{{foo.bar baz bat=1}}').equals(compile('{{foo.bar bar bat=1}}')), false);
equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (bar bat=1)}}')), false);
equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')), false);
equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}')), false);
equal(
compile('{{foo.bar baz bat=1}}').equals(
compile('{{foo.bar bar bat=1}}')
),
false
);
equal(
compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (bar bat=1)}}')
),
false
);
equal(
compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')),
false
);
equal(
compile('{{#foo}} {{/foo}}').equals(
compile('{{#foo}} {{foo}}{{/foo}}')
),
false
);
});
});
describe('#compile', function() {
it('should fail with invalid input', function() {
shouldThrow(function() {
Handlebars.compile(null);
}, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null');
shouldThrow(function() {
Handlebars.compile({});
}, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]');
shouldThrow(
function() {
Handlebars.compile(null);
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
);
shouldThrow(
function() {
Handlebars.compile({});
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
);
});
it('should include the location in the error (row and column)', function() {
try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')();
equal(true, false, 'Statement must throw exception. This line should not be executed.');
equal(
true,
false,
'Statement must throw exception. This line should not be executed.'
);
} catch (err) {
equal(err.message, 'if doesn\'t match def - 2:5', 'Checking error message');
equal(
err.message,
"if doesn't match def - 2:5",
'Checking error message'
);
if (Object.getOwnPropertyDescriptor(err, 'column').writable) {
// In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty,
// its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482)
@@ -57,17 +104,28 @@ describe('compiler', function() {
it('should include the location as enumerable property', function() {
try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')();
equal(true, false, 'Statement must throw exception. This line should not be executed.');
equal(
true,
false,
'Statement must throw exception. This line should not be executed.'
);
} catch (err) {
equal(err.propertyIsEnumerable('column'), true, 'Checking error column');
equal(
Object.prototype.propertyIsEnumerable.call(err, 'column'),
true,
'Checking error column'
);
}
});
it('can utilize AST instance', function() {
equal(Handlebars.compile({
type: 'Program',
body: [ {type: 'ContentStatement', value: 'Hello'}]
})(), 'Hello');
equal(
Handlebars.compile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }]
})(),
'Hello'
);
});
it('can pass through an empty string', function() {
@@ -75,33 +133,52 @@ describe('compiler', function() {
});
it('should not modify the options.data property(GH-1327)', function() {
var options = {data: [{a: 'foo'}, {a: 'bar'}]};
var options = { data: [{ a: 'foo' }, { a: 'bar' }] };
Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)();
equal(JSON.stringify(options, 0, 2), JSON.stringify({data: [{a: 'foo'}, {a: 'bar'}]}, 0, 2));
equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, 0, 2)
);
});
it('should not modify the options.knownHelpers property(GH-1327)', function() {
var options = {knownHelpers: {}};
var options = { knownHelpers: {} };
Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)();
equal(JSON.stringify(options, 0, 2), JSON.stringify({knownHelpers: {}}, 0, 2));
equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ knownHelpers: {} }, 0, 2)
);
});
});
describe('#precompile', function() {
it('should fail with invalid input', function() {
shouldThrow(function() {
Handlebars.precompile(null);
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
shouldThrow(function() {
Handlebars.precompile({});
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]');
shouldThrow(
function() {
Handlebars.precompile(null);
},
Error,
'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null'
);
shouldThrow(
function() {
Handlebars.precompile({});
},
Error,
'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]'
);
});
it('can utilize AST instance', function() {
equal(/return "Hello"/.test(Handlebars.precompile({
type: 'Program',
body: [ {type: 'ContentStatement', value: 'Hello'}]
})), true);
equal(
/return "Hello"/.test(
Handlebars.precompile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }]
})
),
true
);
});
it('can pass through an empty string', function() {
+178 -171
View File
@@ -1,26 +1,24 @@
describe('data', function() {
it('passing in data to a compiled function that expects data - works with helpers', function() {
var template = CompilerContext.compile('{{hello}}', {data: true});
var helpers = {
hello: function(options) {
expectTemplate('{{hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
return options.data.adjective + ' ' + this.noun;
}
};
var result = template({noun: 'cat'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy cat', result, 'Data output by helper');
})
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withInput({ noun: 'cat' })
.withMessage('Data output by helper')
.toCompileTo('happy cat');
});
it('data can be looked up via @foo', function() {
var template = CompilerContext.compile('{{@hello}}');
var result = template({}, { data: { hello: 'hello' } });
equals('hello', result, '@foo retrieves template data');
expectTemplate('{{@hello}}')
.withRuntimeOptions({ data: { hello: 'hello' } })
.withMessage('@foo retrieves template data')
.toCompileTo('hello');
});
it('deep @foo triggers automatic top-level data', function() {
var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
helpers.let = function(options) {
@@ -34,238 +32,247 @@ describe('data', function() {
return options.fn(this, { data: frame });
};
var result = template({ foo: true }, { helpers: helpers });
equals('Hello world', result, 'Automatic data was triggered');
expectTemplate(
'{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'
)
.withInput({ foo: true })
.withHelpers(helpers)
.withMessage('Automatic data was triggered')
.toCompileTo('Hello world');
});
it('parameter data can be looked up via @foo', function() {
var template = CompilerContext.compile('{{hello @world}}');
var helpers = {
hello: function(noun) {
expectTemplate('{{hello @world}}')
.withRuntimeOptions({ data: { world: 'world' } })
.withHelper('hello', function(noun) {
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello world');
});
it('hash values can be looked up via @foo', function() {
var template = CompilerContext.compile('{{hello noun=@world}}');
var helpers = {
hello: function(options) {
expectTemplate('{{hello noun=@world}}')
.withRuntimeOptions({ data: { world: 'world' } })
.withHelper('hello', function(options) {
return 'Hello ' + options.hash.noun;
}
};
var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello world');
});
it('nested parameter data can be looked up via @foo.bar', function() {
var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = {
hello: function(noun) {
expectTemplate('{{hello @world.bar}}')
.withRuntimeOptions({ data: { world: { bar: 'world' } } })
.withHelper('hello', function(noun) {
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { world: {bar: 'world' } } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello world');
});
it('nested parameter data does not fail with @world.bar', function() {
var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = {
hello: function(noun) {
expectTemplate('{{hello @world.bar}}')
.withRuntimeOptions({ data: { foo: { bar: 'world' } } })
.withHelper('hello', function(noun) {
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { foo: {bar: 'world' } } });
equals('Hello undefined', result, '@foo as a parameter retrieves template data');
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello undefined');
});
it('parameter data throws when using complex scope references', function() {
var string = '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
expectTemplate(
'{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}'
).toThrow(Error);
});
it('data can be functions', function() {
var template = CompilerContext.compile('{{@hello}}');
var result = template({}, { data: { hello: function() { return 'hello'; } } });
equals('hello', result);
expectTemplate('{{@hello}}')
.withRuntimeOptions({
data: {
hello: function() {
return 'hello';
}
}
})
.toCompileTo('hello');
});
it('data can be functions with params', function() {
var template = CompilerContext.compile('{{@hello "hello"}}');
var result = template({}, { data: { hello: function(arg) { return arg; } } });
equals('hello', result);
expectTemplate('{{@hello "hello"}}')
.withRuntimeOptions({
data: {
hello: function(arg) {
return arg;
}
}
})
.toCompileTo('hello');
});
it('data is inherited downstream', function() {
var template = CompilerContext.compile('{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}', { data: true });
var helpers = {
let: function(options) {
expectTemplate(
'{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}'
)
.withInput({ bar: { baz: 'hello world' } })
.withCompileOptions({ data: true })
.withHelper('let', function(options) {
var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) {
if (prop in options.hash) {
frame[prop] = options.hash[prop];
}
}
return options.fn(this, {data: frame});
}
};
var result = template({ bar: { baz: 'hello world' } }, { helpers: helpers, data: {} });
equals('2hello world1', result, 'data variables are inherited downstream');
return options.fn(this, { data: frame });
})
.withRuntimeOptions({ data: {} })
.withMessage('data variables are inherited downstream')
.toCompileTo('2hello world1');
});
it('passing in data to a compiled function that expects data - works with helpers in partials', function() {
var template = CompilerContext.compile('{{>myPartial}}', {data: true});
var partials = {
myPartial: CompilerContext.compile('{{hello}}', {data: true})
};
var helpers = {
hello: function(options) {
expectTemplate('{{>myPartial}}')
.withCompileOptions({ data: true })
.withPartial('myPartial', '{{hello}}')
.withHelper('hello', function(options) {
return options.data.adjective + ' ' + this.noun;
}
};
var result = template({noun: 'cat'}, {helpers: helpers, partials: partials, data: {adjective: 'happy'}});
equals('happy cat', result, 'Data output by helper inside partial');
})
.withInput({ noun: 'cat' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Data output by helper inside partial')
.toCompileTo('happy cat');
});
it('passing in data to a compiled function that expects data - works with helpers and parameters', function() {
var template = CompilerContext.compile('{{hello world}}', {data: true});
var helpers = {
hello: function(noun, options) {
expectTemplate('{{hello world}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(noun, options) {
return options.data.adjective + ' ' + noun + (this.exclaim ? '!' : '');
}
};
var result = template({exclaim: true, world: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world!', result, 'Data output by helper');
})
.withInput({ exclaim: true, world: 'world' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Data output by helper')
.toCompileTo('happy world!');
});
it('passing in data to a compiled function that expects data - works with block helpers', function() {
var template = CompilerContext.compile('{{#hello}}{{world}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
expectTemplate('{{#hello}}{{world}}{{/hello}}')
.withCompileOptions({
data: true
})
.withHelper('hello', function(options) {
return options.fn(this);
},
world: function(options) {
})
.withHelper('world', function(options) {
return options.data.adjective + ' world' + (this.exclaim ? '!' : '');
}
};
var result = template({exclaim: true}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world!', result, 'Data output by helper');
})
.withInput({ exclaim: true })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Data output by helper')
.toCompileTo('happy world!');
});
it('passing in data to a compiled function that expects data - works with block helpers that use ..', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: '?'});
},
world: function(thing, options) {
expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
return options.fn({ exclaim: '?' });
})
.withHelper('world', function(thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world?', result, 'Data output by helper');
})
.withInput({ exclaim: true, zomg: 'world' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Data output by helper')
.toCompileTo('happy world?');
});
it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.data.accessData + ' ' + options.fn({exclaim: '?'});
},
world: function(thing, options) {
expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
return options.data.accessData + ' ' + options.fn({ exclaim: '?' });
})
.withHelper('world', function(thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy', accessData: '#win'}});
equals('#win happy world?', result, 'Data output by helper');
})
.withInput({ exclaim: true, zomg: 'world' })
.withRuntimeOptions({ data: { adjective: 'happy', accessData: '#win' } })
.withMessage('Data output by helper')
.toCompileTo('#win happy world?');
});
it('you can override inherited data when invoking a helper', function() {
var template = CompilerContext.compile('{{#hello}}{{world zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: '?', zomg: 'world'}, { data: {adjective: 'sad'} });
},
world: function(thing, options) {
expectTemplate('{{#hello}}{{world zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
return options.fn(
{ exclaim: '?', zomg: 'world' },
{ data: { adjective: 'sad' } }
);
})
.withHelper('world', function(thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: 'planet'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('sad world?', result, 'Overriden data output by helper');
})
.withInput({ exclaim: true, zomg: 'planet' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Overriden data output by helper')
.toCompileTo('sad world?');
});
it('you can override inherited data when invoking a helper with depth', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: '?'}, { data: {adjective: 'sad'} });
},
world: function(thing, options) {
expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
return options.fn({ exclaim: '?' }, { data: { adjective: 'sad' } });
})
.withHelper('world', function(thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('sad world?', result, 'Overriden data output by helper');
})
.withInput({ exclaim: true, zomg: 'world' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Overriden data output by helper')
.toCompileTo('sad world?');
});
describe('@root', function() {
it('the root context can be looked up via @root', function() {
var template = CompilerContext.compile('{{@root.foo}}');
var result = template({foo: 'hello'}, { data: {} });
equals('hello', result);
expectTemplate('{{@root.foo}}')
.withInput({ foo: 'hello' })
.withRuntimeOptions({ data: {} })
.toCompileTo('hello');
result = template({foo: 'hello'}, {});
equals('hello', result);
expectTemplate('{{@root.foo}}')
.withInput({ foo: 'hello' })
.toCompileTo('hello');
});
it('passed root values take priority', function() {
var template = CompilerContext.compile('{{@root.foo}}');
var result = template({}, { data: {root: {foo: 'hello'} } });
equals('hello', result);
expectTemplate('{{@root.foo}}')
.withInput({ foo: 'should not be used' })
.withRuntimeOptions({ data: { root: { foo: 'hello' } } })
.toCompileTo('hello');
});
});
describe('nesting', function() {
it('the root context can be looked up via @root', function() {
var template = CompilerContext.compile('{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}');
var result = template({foo: 'hello'}, {
helpers: {
helper: function(options) {
var frame = Handlebars.createFrame(options.data);
frame.depth = options.data.depth + 1;
return options.fn(this, {data: frame});
expectTemplate(
'{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}'
)
.withInput({ foo: 'hello' })
.withHelper('helper', function(options) {
var frame = Handlebars.createFrame(options.data);
frame.depth = options.data.depth + 1;
return options.fn(this, { data: frame });
})
.withRuntimeOptions({
data: {
depth: 0
}
},
data: {
depth: 0
}
});
equals('2 1 0', result);
})
.toCompileTo('2 1 0');
});
});
});
+13 -2
View File
@@ -1,7 +1,15 @@
require('./common');
var fs = require('fs'),
vm = require('vm');
vm = require('vm');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = 'no-conflict';
@@ -9,7 +17,10 @@ var filename = 'dist/handlebars.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.min.js';
}
var distHandlebars = fs.readFileSync(require.resolve('../../' + filename), 'utf-8');
var distHandlebars = fs.readFileSync(
require.resolve('../../' + filename),
'utf-8'
);
vm.runInThisContext(distHandlebars, filename);
global.CompilerContext = {
+175 -13
View File
@@ -1,4 +1,6 @@
var global = (function() { return this; }());
var global = (function() {
return this;
})();
var AssertError;
if (Error.captureStackTrace) {
@@ -16,29 +18,48 @@ if (Error.captureStackTrace) {
AssertError = Error;
}
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) {
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.shouldCompileToWithPartials = function shouldCompileToWithPartials(
string,
hashOrArray,
partials,
expected,
message
) {
var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) {
throw new AssertError("'" + result + "' should === '" + expected + "': " + message, shouldCompileToWithPartials);
throw new AssertError(
"'" + result + "' should === '" + expected + "': " + message,
shouldCompileToWithPartials
);
}
};
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.compileWithPartials = function(string, hashOrArray, partials) {
var template,
ary,
options;
var template, ary, options;
if (hashOrArray && hashOrArray.hash) {
ary = [hashOrArray.hash, hashOrArray];
delete hashOrArray.hash;
} else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') {
ary = [];
ary.push(hashOrArray[0]);
ary.push(hashOrArray[0]); // input
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
options = typeof hashOrArray[3] === 'object' ? hashOrArray[3] : {compat: hashOrArray[3]};
options =
typeof hashOrArray[3] === 'object'
? hashOrArray[3]
: { compat: hashOrArray[3] };
if (hashOrArray[4] != null) {
options.data = !!hashOrArray[4];
ary[1].data = hashOrArray[4];
@@ -47,17 +68,30 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
ary = [hashOrArray];
}
template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string, options);
template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](
string,
options
);
return template.apply(this, ary);
};
/**
* @deprecated Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`)
* @see https://www.chaijs.com/api/bdd/
*/
global.equals = global.equal = function equals(a, b, msg) {
if (a !== b) {
throw new AssertError("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''), equals);
throw new AssertError(
"'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''),
equals
);
}
};
/**
* @deprecated Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`)
* @see https://www.chaijs.com/api/bdd/#method_throw
*/
global.shouldThrow = function(callback, type, msg) {
var failed;
try {
@@ -67,11 +101,139 @@ global.shouldThrow = function(callback, type, msg) {
if (type && !(caught instanceof type)) {
throw new AssertError('Type failure: ' + caught);
}
if (msg && !(msg.test ? msg.test(caught.message) : msg === caught.message)) {
throw new AssertError('Throw mismatch: Expected ' + caught.message + ' to match ' + msg + '\n\n' + caught.stack, shouldThrow);
if (
msg &&
!(msg.test ? msg.test(caught.message) : msg === caught.message)
) {
throw new AssertError(
'Throw mismatch: Expected ' +
caught.message +
' to match ' +
msg +
'\n\n' +
caught.stack,
shouldThrow
);
}
}
if (failed) {
throw new AssertError('It failed to throw', shouldThrow);
}
};
global.expectTemplate = function(templateAsString) {
return new HandlebarsTestBench(templateAsString);
};
function HandlebarsTestBench(templateAsString) {
this.templateAsString = templateAsString;
this.helpers = {};
this.partials = {};
this.decorators = {};
this.input = {};
this.message =
'Template' + templateAsString + ' does not evaluate to expected output';
this.compileOptions = {};
this.runtimeOptions = {};
}
HandlebarsTestBench.prototype.withInput = function(input) {
this.input = input;
return this;
};
HandlebarsTestBench.prototype.withHelper = function(name, helperFunction) {
this.helpers[name] = helperFunction;
return this;
};
HandlebarsTestBench.prototype.withHelpers = function(helperFunctions) {
var self = this;
Object.keys(helperFunctions).forEach(function(name) {
self.withHelper(name, helperFunctions[name]);
});
return this;
};
HandlebarsTestBench.prototype.withPartial = function(name, partialAsString) {
this.partials[name] = partialAsString;
return this;
};
HandlebarsTestBench.prototype.withPartials = function(partials) {
var self = this;
Object.keys(partials).forEach(function(name) {
self.withPartial(name, partials[name]);
});
return this;
};
HandlebarsTestBench.prototype.withDecorator = function(
name,
decoratorFunction
) {
this.decorators[name] = decoratorFunction;
return this;
};
HandlebarsTestBench.prototype.withDecorators = function(decorators) {
var self = this;
Object.keys(decorators).forEach(function(name) {
self.withDecorator(name, decorators[name]);
});
return this;
};
HandlebarsTestBench.prototype.withCompileOptions = function(compileOptions) {
this.compileOptions = compileOptions;
return this;
};
HandlebarsTestBench.prototype.withRuntimeOptions = function(runtimeOptions) {
this.runtimeOptions = runtimeOptions;
return this;
};
HandlebarsTestBench.prototype.withMessage = function(message) {
this.message = message;
return this;
};
HandlebarsTestBench.prototype.toCompileTo = function(expectedOutputAsString) {
expect(this._compileAndExecute()).to.equal(
expectedOutputAsString,
this.message
);
};
// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
HandlebarsTestBench.prototype.toThrow = function(errorLike, errMsgMatcher) {
var self = this;
expect(function() {
self._compileAndExecute();
}).to.throw(errorLike, errMsgMatcher, this.message);
};
HandlebarsTestBench.prototype._compileAndExecute = function() {
var compile =
Object.keys(this.partials).length > 0
? CompilerContext.compileWithPartial
: CompilerContext.compile;
var combinedRuntimeOptions = this._combineRuntimeOptions();
var template = compile(this.templateAsString, this.compileOptions);
return template(this.input, combinedRuntimeOptions);
};
HandlebarsTestBench.prototype._combineRuntimeOptions = function() {
var self = this;
var combinedRuntimeOptions = {};
Object.keys(this.runtimeOptions).forEach(function(key) {
combinedRuntimeOptions[key] = self.runtimeOptions[key];
});
combinedRuntimeOptions.helpers = this.helpers;
combinedRuntimeOptions.partials = this.partials;
combinedRuntimeOptions.decorators = this.decorators;
return combinedRuntimeOptions;
};
+8
View File
@@ -1,5 +1,13 @@
require('./common');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = require('../../lib');
global.CompilerContext = {
+12 -8
View File
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
var fs = require('fs'),
Mocha = require('mocha'),
path = require('path');
Mocha = require('mocha'),
path = require('path');
var errors = 0,
testDir = path.dirname(__dirname),
grep = process.argv[2];
testDir = path.dirname(__dirname),
grep = process.argv[2];
// Lazy hack, but whatever
if (grep === '--min') {
@@ -13,9 +13,14 @@ if (grep === '--min') {
grep = undefined;
}
var files = fs.readdirSync(testDir)
.filter(function(name) { return (/.*\.js$/).test(name); })
.map(function(name) { return testDir + path.sep + name; });
var files = fs
.readdirSync(testDir)
.filter(function(name) {
return /.*\.js$/.test(name);
})
.map(function(name) {
return testDir + path.sep + name;
});
if (global.minimizedTest) {
run('./runtime', function() {
@@ -37,7 +42,6 @@ if (global.minimizedTest) {
});
}
function run(env, callback) {
var mocha = new Mocha();
mocha.ui('bdd');
+18 -3
View File
@@ -1,7 +1,15 @@
require('./common');
var fs = require('fs'),
vm = require('vm');
vm = require('vm');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = 'no-conflict';
@@ -9,7 +17,10 @@ var filename = 'dist/handlebars.runtime.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.runtime.min.js';
}
vm.runInThisContext(fs.readFileSync(__dirname + '/../../' + filename), filename);
vm.runInThisContext(
fs.readFileSync(__dirname + '/../../' + filename),
filename
);
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
@@ -21,7 +32,11 @@ global.CompilerContext = {
compile: function(template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function(precompileTemplate, precompileOptions) {
return compiler.precompile(precompileTemplate, precompileOptions, handlebarsEnv);
return compiler.precompile(
precompileTemplate,
precompileOptions,
handlebarsEnv
);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
+6
View File
@@ -0,0 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "a";
},"useData":true});
});
+3
View File
@@ -0,0 +1,3 @@
{"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test String</div>";
},"useData":true}
+1
View File
@@ -0,0 +1 @@
define(["handlebars.runtime"],function(e){var t=(e=e.default).template;return(e.templates=e.templates||{}).empty=t({compiler:[8,">= 4.3.0"],main:function(e,t,a,n,r){return""},useData:!0})});
+6
View File
@@ -0,0 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+3
View File
@@ -0,0 +1,3 @@
{"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true}
+6
View File
@@ -0,0 +1,6 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
})();
+10
View File
@@ -0,0 +1,10 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>1</div>";
},"useData":true});
templates['secondTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>2</div>";
},"useData":true});
return templates;
});
+6
View File
@@ -0,0 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['artifacts/partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});

Some files were not shown because too many files have changed in this diff Show More